Automatically setting the record type of a detail object based on the record type of its master object

Record types are defined on individual objects and when an object that has a record type is created using the default UI the user is first presented with a page where the record type is selected and then is presented with a normal edit page controlled by the selected record type. But if an object model has master and detail objects where the record types need to be consistent, this manual setting is inappropriate for the detail object because it allows an inconsistent value to be set and also because it forces the user to fill in an extra, redundant page.

This post describes a technique for skipping the manual setting of the record type for the detail object and instead automatically setting it. An important part of this is the “Skip Record Type Selection Page” option accessed via an object’s “Standard Buttons and Links” override for “New”:

The normal “New” page is replaced by the specified page that must be based on the object’s standard controller (to be offered in the override page) and add its logic via a controller extension:

        action="{! url }"

The page is never rendered but instead invokes its controller’s url method that returns the URL that is rendered.

The controller is implemented in two classes to separate the core logic that applies to any master/detail pair from the specific master/detail information. Here BenefitClaimed is the master object and PaymentSpecification is the detail object (created using the “New” button above the related list of PaymentSpecification objects in the BenefitClaimed page):

public with sharing class PaymentSpecificationNewController extends NewChildControllerBase {
    public PaymentSpecificationNewController(ApexPages.StandardController ignored) {
        super(BenefitClaimed__c.SObjectType, PaymentSpecification__c.SObjectType);

The base class contains the core logic:

public abstract class NewChildControllerBase {
    private SObjectType parentSobType;
    private SObjectType childSobType;
    public NewChildControllerBase(SObjectType parentSobType, SObjectType childSobType) {
        this.parentSobType = parentSobType;
        this.childSobType = childSobType;
    public PageReference url() {
        PageReference p = new PageReference('/' + childSobType.getDescribe().getKeyPrefix() + '/e');
        Map<String, String> m = p.getParameters();
        m.put('RecordType', getChildRecordTypeId(getParentSObjectId(m)));
        m.put('nooverride', '1');
        return p;
    private Id getChildRecordTypeId(Id parentId) {
        SObject parent = Database.query('select RecordType.DeveloperName from ' + String.valueOf(parentSobType)
                + ' where Id = :parentId');
        String parentDeveloperName = (String) parent.getSobject('RecordType').get('DeveloperName');
        return [select Id from RecordType where SObjectType = :String.valueOf(childSobType)
                and DeveloperName = :parentDeveloperName].Id;
    private Id getParentSObjectId(Map<String, String> m) {
        for (String key : m.keySet()) {
            if (key.endsWith('_lkid')) {
                return m.get(key);
        return null;

The approach used is to take all the parameters from the original request, then add the parameters RecordType and nooverride, and then use those parameters with the normal object edit URL. Keeping the original request parameters is important because they include the values needed to pre-populate the parent object name and id in the child object (see Lkid hack value is fragile for some backround information on this; the approach used here is not fragile) and the URL to return to on cancel. The RecordType parameter supplies the detail object’s record type id and the nooverride parameter stops an infinite loop by ensuring the request is handled by the default UI page rather than the override page.

The record type id for the detail object is obtained by using the id of the master object, obtained from the “_lkid” suffixed parameter, to query the master object’s record type. This is then used to obtain the record type id for the detail object. In this case both record types have the same DeveloperName so that is used in the matching logic but other mappings can obviously be implemented. The dynamic SOQL is used to keep this code generic and configured by just the two SObjectType constructor parameters.