LearnFrenzy
Sign in Take a free test

sObjects, SOQL and SOSL · Lesson 14 of 41

sObjects in Apex: Working with Records

5 min read Updated Nov 8, 2026 Saurabh Samir

Every Salesforce record you touch in Apex is an sObject: an Account, a Contact, a custom Invoice__c. Each object in your org automatically becomes an Apex type, with its fields as properties. That's what makes Apex feel so direct – and understanding sObjects (including the generic SObject type) is the foundation for SOQL, DML and triggers.

In this lesson

  • Specific sObject types and their fields
  • Lookup fields vs relationship fields
  • The generic SObject type with get and put
  • Schema describe: learning about objects at runtime

Creating sObjects

// three ways to set fields
Account acc = new Account(Name = 'Acme Corp', Industry = 'Energy');   // name = value pairs
acc.AnnualRevenue = 5000000;                                            // dot notation
acc.put('Rating', 'Hot');                                               // generic put by name

System.debug(acc);
System.debug('Saved yet? ' + (acc.Id != null));

insert acc;                                   // saving fills in the Id
System.debug('Saved now? ' + (acc.Id != null) + ', prefix ' + String.valueOf(acc.Id).left(3));

// a field you never set or queried is simply null
System.debug('Phone: ' + acc.Phone);

Debug log

Account:{Name=Acme Corp, Industry=Energy, AnnualRevenue=5000000, Rating=Hot}
Saved yet? false
Saved now? true, prefix 001
Phone: null

An sObject in memory is just data until you save it with DML. Custom objects and fields work the same way, with their API names: new Invoice__c(Amount__c = 500).

Lookup fields and relationship fields

A lookup has two faces. The Id field (AccountId, Customer__c) holds the parent's Id and is what you set when saving. The relationship field (Account, Customer__r) holds the parent record itself – it's only filled when you query it:

Account parent = new Account(Name = 'Globex');
insert parent;

Contact c = new Contact(LastName = 'Rao', AccountId = parent.Id);    // set the Id field to link
insert c;

Contact loaded = [SELECT LastName, AccountId, Account.Name, Account.CreatedDate FROM Contact WHERE Id = :c.Id];
System.debug('AccountId set: ' + (loaded.AccountId == parent.Id));
System.debug('Parent name via relationship: ' + loaded.Account.Name);

Contact notQueried = [SELECT LastName FROM Contact WHERE Id = :c.Id];
try {
    System.debug(notQueried.Account.Name);              // relationship not in the SELECT
} catch (SObjectException e) {
    System.debug(e.getMessage());
}

Debug log

AccountId set: true
Parent name via relationship: Globex
SObject row was retrieved via SOQL without querying the requested field: Contact.Account

The last line is worth remembering: reading a field – or a relationship – that wasn't in the query's SELECT list doesn't give you null, it throws an SObjectException. When code fails with "SObject row was retrieved via SOQL without querying the requested field", add the field to the query.

Standard objectCustom lookup
Id field (set this to link)Contact.AccountIdInvoice__c.Customer__c
Relationship field (query this to read the parent)Contact.Account.NameInvoice__c.Customer__r.Name
Child relationship (subquery from the parent)Account.ContactsAccount.Invoices__r

The generic SObject type

SObject can hold any record. You lose compile-time field checks, but gain the ability to write code that works with any object – a field-copy utility, a generic logger, a configurable mapping:

SObject rec = Schema.getGlobalDescribe().get('Lead').newSObject();     // create by object name
rec.put('LastName', 'Shah');
rec.put('Company', 'Initech');
rec.put('NumberOfEmployees', 85);

System.debug('Type: ' + rec.getSObjectType());
System.debug('Company: ' + rec.get('Company'));

// copy several fields by name – the kind of code that drives field mappings
Map<String, String> mapping = new Map<String, String>{ 'Company' => 'Name', 'NumberOfEmployees' => 'NumberOfEmployees' };
Account target = new Account();
for (String src : mapping.keySet()) {
    target.put(mapping.get(src), rec.get(src));
}
System.debug(target);

// get() returns Object – cast to use it
Integer employees = (Integer) rec.get('NumberOfEmployees');
System.debug(employees + 15);

Debug log

Type: Lead
Company: Initech
Account:{Name=Initech, NumberOfEmployees=85}
100

Schema describe: learn about objects at runtime

Schema.DescribeSObjectResult accDescribe = Account.SObjectType.getDescribe();
System.debug('Label: ' + accDescribe.getLabel() + ' / ' + accDescribe.getLabelPlural());
System.debug('Key prefix: ' + accDescribe.getKeyPrefix());
System.debug('Can the user create Accounts? ' + accDescribe.isCreateable());

Schema.DescribeFieldResult ratingField = Account.Rating.getDescribe();
System.debug('Rating type: ' + ratingField.getType());
List<String> values = new List<String>();
for (Schema.PicklistEntry pe : ratingField.getPicklistValues()) values.add(pe.getValue());
System.debug('Rating values: ' + values);

Map<String, Schema.SObjectField> fields = Schema.SObjectType.Contact.fields.getMap();
System.debug('Contact has an Email field? ' + fields.containsKey('email'));
System.debug('Email is ' + fields.get('email').getDescribe().getType());

Debug log

Label: Account / Accounts
Key prefix: 001
Can the user create Accounts? true
Rating type: PICKLIST
Rating values: (Hot, Warm, Cold)
Contact has an Email field? true
Email is EMAIL

Describe calls power dynamic UIs (picklist options for a component), security checks (isAccessible(), isUpdateable()) and generic code. Field map keys are lower-case, as the example shows. Describe results are cached by the platform, but in loops prefer calling them once and storing the result.

Practice

Try it yourself

  1. You set contact.Account = new Account(Name = 'X'); and insert the contact, but it isn't linked to any account. Why?

    Show answer

    Linking uses the Id field: set contact.AccountId to an existing account's Id. The relationship field is for reading queried data (or for upserting by an external Id, a special case).

  2. Write a method that returns the picklist values of any field, given the object and field names as Strings.

    Show answer

    Schema.SObjectField f = Schema.getGlobalDescribe().get(objName).getDescribe().fields.getMap().get(fieldName); List<String> out = new List<String>(); for (Schema.PicklistEntry pe : f.getDescribe().getPicklistValues()) out.add(pe.getValue()); return out;

Quick check

What does a custom lookup's relationship field end with?

__r (the Id field ends with __c).

When is acc.Id filled in?

After the record is inserted (or when it's queried).

What does SObject.get() return?

An Object – cast it to the field's type.

Summary

  • Every object is an Apex type; fields are properties with API names.
  • Set the Id field to link records; query the relationship field to read the parent.
  • The generic SObject type with get/put enables code that works with any object.
  • Schema describe gives labels, field types, picklist values and permissions at runtime.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links