Salesforce

Field Sets in Salesforce: Dynamic Field Display in Apex

A Field Set is a grouping of fields defined on an object in Salesforce Setup. Instead of hard-coding which fields appear in a page or query, you reference the Field Set by name, and an administrator can add, remove, or reorder fields without a developer touching code. That separation of concerns is the entire value proposition.

Why Field Sets Matter

Imagine you ship a Visualforce page or an LWC component that displays ten account fields. Three months later, the business wants two more fields shown and one removed. Without Field Sets, that is a code change, a deployment, and a release cycle. With Field Sets, an admin opens Setup, edits the Field Set, and the page reflects the change immediately — zero code touched.

  • Delegates field configuration to admins without exposing code.
  • Supports multiple layouts driven by different Field Sets on the same component.
  • Works with Visualforce, Apex, and Lightning Web Components.
  • Respects field-level security — fields hidden from a user are automatically excluded.
  • Survives managed package upgrades: subscriber orgs can add custom fields to a Field Set without modifying the package.

Creating a Field Set in Setup

  1. Navigate to Setup → Object Manager and open the object (e.g., Account).
  2. Select Field Sets from the left sidebar.
  3. Click New.
  4. Give it a Field Set Label (e.g., "Account Summary") and a Field Set Name (API name, e.g., Account_Summary).
  5. Drag fields from the object palette into the In the Field Set area. Fields you drag to Available for the Field Set are accessible via Apex but not shown by default.
  6. Save.

The API name you chose (Account_Summary) is how you reference the Field Set in both Apex and Visualforce. It is case-sensitive in Apex.

Reading a Field Set in Apex

The Schema namespace exposes Field Sets through the SObjectType describe result. The key classes are Schema.FieldSet and Schema.FieldSetMember.

public class FieldSetDemo {

    /**
     * Returns the list of FieldSetMember objects for a given
     * object API name and Field Set name.
     */
    public static List<Schema.FieldSetMember> getMembers(
            String objectApiName,
            String fieldSetName) {

        Schema.SObjectType targetType =
            Schema.getGlobalDescribe().get(objectApiName);

        if (targetType == null) {
            throw new IllegalArgumentException(
                'Unknown object: ' + objectApiName);
        }

        Schema.DescribeSObjectResult describeResult =
            targetType.getDescribe();

        Map<String, Schema.FieldSet> fieldSets =
            describeResult.fieldSets.getMap();

        if (!fieldSets.containsKey(fieldSetName)) {
            throw new IllegalArgumentException(
                'Unknown Field Set: ' + fieldSetName +
                ' on ' + objectApiName);
        }

        return fieldSets.get(fieldSetName).getFields();
    }
}

Schema.FieldSetMember exposes several important properties:

  • getFieldPath() — the API field name (e.g., Name, AnnualRevenue). For span fields, this can be a dotted path like Account.Name.
  • getLabel() — the display label you see in the UI.
  • getType() — returns a Schema.DisplayType enum value (e.g., STRING, CURRENCY).
  • getRequired()true if the field is marked required in the Field Set definition.
  • getDBRequired()true if the underlying field is required at the database level.
  • isRequired() — convenience check: true if either getRequired() or getDBRequired() is true.
// Print each member's info to debug log
List<Schema.FieldSetMember> members =
    FieldSetDemo.getMembers('Account', 'Account_Summary');

for (Schema.FieldSetMember m : members) {
    System.debug(
        'Field: '    + m.getFieldPath()  +
        ' | Label: ' + m.getLabel()      +
        ' | Type: '  + m.getType()       +
        ' | Req: '   + m.isRequired()
    );
}

Building Dynamic SOQL from a Field Set

The most common Apex pattern is constructing a SOQL query at runtime using the Field Set members. This ensures the query always fetches exactly the fields the admin has configured.

public class AccountFieldSetQuery {

    public static List<SObject> queryWithFieldSet(
            String fieldSetName,
            List<Id> recordIds) {

        // 1. Gather field API names from the Field Set
        List<Schema.FieldSetMember> members =
            FieldSetDemo.getMembers('Account', fieldSetName);

        Set<String> fieldNames = new Set<String>{'Id'}; // Id is always needed
        for (Schema.FieldSetMember m : members) {
            fieldNames.add(m.getFieldPath());
        }

        // 2. Build the SELECT clause
        String selectClause = String.join(
            new List<String>(fieldNames), ', ');

        // 3. Bind Ids safely — never concatenate user-supplied values
        String soql =
            'SELECT '  + selectClause  +
            ' FROM Account' +
            ' WHERE Id IN :recordIds';

        // 4. Execute and return
        return Database.query(soql);
    }
}

Note the deliberate use of a bind variable (:recordIds) rather than string concatenation for the WHERE clause. Field names from a Field Set are admin-controlled metadata, not user input, so they are safe to interpolate into the SELECT clause. User-supplied values must always be bound.

Calling the dynamic query

List<Id> ids = new List<Id>{
    '001000000000001AAA',
    '001000000000002AAA'
};

List<SObject> results =
    AccountFieldSetQuery.queryWithFieldSet('Account_Summary', ids);

for (SObject rec : results) {
    System.debug(rec);
}

Field Sets in Visualforce

Visualforce has first-class support for Field Sets via the apex:repeat component and the $ObjectType global variable. No Apex controller code is needed for simple read-only rendering.

<!-- AccountFieldSetPage.page -->
<apex:page standardController="Account">
    <apex:pageBlock title="Account Summary">
        <apex:pageBlockSection columns="2">
            <apex:repeat
                value="{!$ObjectType.Account.FieldSets.Account_Summary}"
                var="f">
                <apex:outputField value="{!Account[f]}" />
            </apex:repeat>
        </apex:pageBlockSection>
    </apex:pageBlock>
</apex:page>

{!Account[f]} uses the dynamic binding syntax. The f variable is a FieldSetMember, and Visualforce knows how to resolve it against the record. apex:outputField automatically renders the correct input type and respects field-level security.

Editable Field Set form in Visualforce

<apex:page standardController="Account" sidebar="false">
    <apex:form>
        <apex:pageBlock title="Edit Account" mode="edit">
            <apex:pageBlockSection columns="1">
                <apex:repeat
                    value="{!$ObjectType.Account.FieldSets.Account_Summary}"
                    var="f">
                    <apex:inputField
                        value="{!Account[f]}"
                        required="{!OR(f.DBRequired, f.Required)}" />
                </apex:repeat>
            </apex:pageBlockSection>
            <apex:pageBlockButtons>
                <apex:commandButton value="Save" action="{!save}" />
                <apex:commandButton value="Cancel" action="{!cancel}" />
            </apex:pageBlockButtons>
        </apex:pageBlock>
    </apex:form>
</apex:page>

Field Sets in Lightning Web Components

LWC does not have native Field Set support the way Visualforce does. You must wire up an Apex method that reads the Field Set and return the field metadata to the component. Then use lightning-record-view-form or lightning-record-edit-form with lightning-output-field / lightning-input-field.

Step 1 — Apex controller

public with sharing class FieldSetController {

    public class FieldSetInfo {
        @AuraEnabled public String fieldPath;
        @AuraEnabled public String label;
        @AuraEnabled public Boolean required;
    }

    @AuraEnabled(cacheable=true)
    public static List<FieldSetInfo> getFieldSetFields(
            String objectApiName,
            String fieldSetName) {

        List<Schema.FieldSetMember> members =
            FieldSetDemo.getMembers(objectApiName, fieldSetName);

        List<FieldSetInfo> result = new List<FieldSetInfo>();
        for (Schema.FieldSetMember m : members) {
            FieldSetInfo info  = new FieldSetInfo();
            info.fieldPath = m.getFieldPath();
            info.label     = m.getLabel();
            info.required  = m.isRequired();
            result.add(info);
        }
        return result;
    }
}

Step 2 — LWC JavaScript

// fieldSetViewer.js
import { LightningElement, api, wire } from 'lwc';
import getFieldSetFields from '@salesforce/apex/FieldSetController.getFieldSetFields';

export default class FieldSetViewer extends LightningElement {
    @api recordId;
    @api objectApiName;
    @api fieldSetName = 'Account_Summary';

    fields = [];
    error;

    @wire(getFieldSetFields, {
        objectApiName: '$objectApiName',
        fieldSetName:  '$fieldSetName'
    })
    wiredFields({ data, error }) {
        if (data) {
            this.fields = data;
            this.error  = undefined;
        } else if (error) {
            this.error  = error;
            this.fields = [];
        }
    }
}

Step 3 — LWC HTML template

<!-- fieldSetViewer.html -->
<template>
    <lightning-card title="Record Details">
        <div class="slds-p-around_medium">
            <template lwc:if={fields.length}>
                <lightning-record-view-form
                    record-id={recordId}
                    object-api-name={objectApiName}>
                    <template for:each={fields} for:item="f">
                        <lightning-output-field
                            key={f.fieldPath}
                            field-name={f.fieldPath}>
                        </lightning-output-field>
                    </template>
                </lightning-record-view-form>
            </template>
            <template lwc:if={error}>
                <p class="slds-text-color_error">
                    Failed to load fields.
                </p>
            </template>
        </div>
    </lightning-card>
</template>

Step 4 — Component metadata

<!-- fieldSetViewer.js-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>62.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <property name="fieldSetName" type="String"
                       label="Field Set Name"
                       default="Account_Summary" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Exposing fieldSetName as a design property means admins can configure which Field Set the component uses directly from the Lightning App Builder — no code change required.

Unit Testing Field Set Apex

Schema.FieldSet and Schema.FieldSetMember are real metadata objects that exist in any org. You do not need to mock them. Your test simply calls the method and asserts the result is a non-empty list. The Field Set must exist in the test org (including scratch orgs with the source deployed).

@isTest
private class FieldSetDemoTest {

    @isTest
    static void getMembers_returnsNonEmptyList() {
        // Requires 'Account_Summary' Field Set deployed in the test org
        Test.startTest();
        List<Schema.FieldSetMember> members =
            FieldSetDemo.getMembers('Account', 'Account_Summary');
        Test.stopTest();

        System.assertNotEquals(
            0,
            members.size(),
            'Field Set should contain at least one field'
        );
    }

    @isTest
    static void getMembers_throwsOnUnknownObject() {
        try {
            FieldSetDemo.getMembers('FakeObject__c', 'SomeFieldSet');
            System.assert(false, 'Expected exception not thrown');
        } catch (IllegalArgumentException e) {
            System.assert(
                e.getMessage().contains('Unknown object'),
                'Wrong exception message: ' + e.getMessage()
            );
        }
    }

    @isTest
    static void getMembers_throwsOnUnknownFieldSet() {
        try {
            FieldSetDemo.getMembers('Account', 'NonExistent_FS');
            System.assert(false, 'Expected exception not thrown');
        } catch (IllegalArgumentException e) {
            System.assert(
                e.getMessage().contains('Unknown Field Set'),
                'Wrong exception message: ' + e.getMessage()
            );
        }
    }
}

Field Sets in Managed Packages

Field Sets are a first-class packaging construct. When you include a Field Set in a managed package, subscribers can extend it by adding their own custom fields to the "Available" section. This is the recommended extensibility pattern for ISVs — you ship a default set of fields, and customers tailor it without forking your component code.

  • Include the Field Set in your package.xml under the FieldSet metadata type.
  • Use the namespace prefix when referencing it in Apex: YourNS__Account_Summary.
  • Subscribers add fields to the "Available for the Field Set" tray; they cannot remove your required fields.
<!-- package.xml snippet -->
<types>
    <members>Account.Account_Summary</members>
    <name>FieldSet</name>
</types>

Common Pitfalls

  • Field-level security gapsSchema.FieldSetMember respects FLS at query time when used in Visualforce's dynamic binding. In Apex, call stripInaccessible() or check isAccessible() yourself before exposing data.
  • Dotted paths in dynamic SOQL — Relationship fields like Owner.Name are valid in a Field Set but require a subquery or alias in SOQL. Parse the field path and handle the dot case explicitly.
  • Missing Field Set in scratch org — When running tests against a scratch org, ensure the Field Set is part of your source deployment. Tests that hit Schema.getGlobalDescribe() for a non-existent Field Set will throw at runtime, not compile time.
  • Governor limits on getGlobalDescribe() — Calling getGlobalDescribe() in a tight loop is expensive. Cache the result in a static variable within the same transaction.
  • Namespace mismatch — In namespaced packages, the Field Set API name is prefixed. Always use the fully qualified name when calling getFields() from managed package code.

Caching the describe result

public class DescribeCache {

    private static final Map<String, Schema.SObjectType> GLOBAL_DESCRIBE =
        Schema.getGlobalDescribe();

    public static Schema.DescribeSObjectResult getSObjectDescribe(
            String objectApiName) {
        Schema.SObjectType t = GLOBAL_DESCRIBE.get(objectApiName);
        if (t == null) {
            throw new IllegalArgumentException(
                'Unknown object: ' + objectApiName);
        }
        return t.getDescribe();
    }
}

By storing Schema.getGlobalDescribe() in a static final field, you pay the describe cost once per transaction no matter how many methods call it.

Summary

Use Field Sets whenever field selection needs to be configurable by admins at the object level without a code deployment. They are the right tool for any page or component where the displayed fields may change over the lifetime of the application — intake forms, summary views, search result tables, or managed package UI. The core pattern is always the same: read Schema.FieldSetMember objects via describeResult.fieldSets.getMap(), extract the getFieldPath() values, and build your SOQL or drive your template from that list. Remember to cache global describe results, handle the namespace prefix in packaged code, and always strip or check field-level security when returning data from an Apex @AuraEnabled method.

← All articles

Frequently Asked Questions

Can a Field Set be used to build dynamic SOQL queries in Apex?

Yes. By iterating over the list of Schema.FieldSetMember objects returned by getFields(), you can call getFieldPath() on each member to collect field API names and concatenate them into a SOQL SELECT clause at runtime. This means the query automatically expands or contracts as an admin adds or removes fields from the Field Set in Setup, with no code changes required.

What is the difference between fields dragged to 'In the Field Set' versus 'Available for the Field Set'?

Fields in the 'In the Field Set' area are the required, visible members returned by getFields() and displayed by default in any component or page that consumes the Field Set. Fields placed in 'Available for the Field Set' are accessible through the same Apex API but are not shown unless the consuming code explicitly checks the isRequired() or dbRequired() flags and handles them separately — they act as an optional pool an admin can promote without a developer.

Does referencing a Field Set in Apex automatically enforce field-level security for the running user?

Field Sets respect field-level security at the metadata level — if a field is hidden from a user's profile, Salesforce excludes it from the Field Set member list returned at runtime for that user. However, Apex code running in system mode (without sharing keywords) still queries data in a privileged context, so developers must also enforce CRUD and FLS checks explicitly if the query is executed in system mode rather than user mode.

How are Field Sets handled when a managed package is installed in a subscriber org?

The package ships with its own Field Set definition, but subscriber orgs can add their custom fields to that Field Set through Setup without modifying the package itself. Because the Field Set is read dynamically at runtime via Schema.DescribeSObjectResult, those subscriber-added fields appear automatically in any Apex or Visualforce logic that iterates the Field Set members — no repackaging or redeployment is needed from the ISV.