Salesforce

Field Sets in Salesforce: Dynamic Field Display in Apex

```html

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.

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:

// 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.

<!-- package.xml snippet -->
<types>
    <members>Account.Account_Summary</members>
    <name>FieldSet</name>
</types>

Common Pitfalls

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