[SalesForce] How to deserialize into interface type when concrete type is unknown

As a follow up to this question:

I am essentially trying to deserialize a JSON object into an Interface Type.

Unfortunately we cannot deserialize using an Interface as the ApexType and must know the concrete class type to do so then cast to the interface….

As part of the interface there is a method to enforce returning the Name of the class but this only works when the Object is passed to aura and back to apex.

Using straight Apex when serializing the Class that implements the interface the property getter getName() is not serialized and thus is null on deserialization into a Map<String,Object>.

I am using the following method to get the class name from a class that implements the interface to be able to deserialize it:

/**
 * Gets the Name property from a class
 *
 * @param objectJSON
 *
 * @return
 */
@TestVisible private static String getClassName(String objectJSON){
    Map<String,Object> tmp = (Map<String, Object>) JSON.deserializeUntyped(objectJSON);
    //Just for debugging purposes
    for(String key : tmp.keySet()){
        System.debug('Key: ' + key + ' - Value: ' + tmp.get(key));
    }

    String typeName = (String) ( (Map<String, Object>) JSON.deserializeUntyped(objectJSON) ).get('name');

    System.debug('Type Name:' + typeName);

    if(String.isBlank(typeName)){
        throw new auraController_Exception('Well, this did not work as the name property was blank');
    }

    return typeName;


}

In the apex method I have a JSON serialized string of the class that implements the interface and I am trying to deserialize it into the interface type. The class name is unknown at this time and the interface requires a method getName() to return the class name. This line calls the above method

theInterface controller = (theInterface) json.deserialize(objectJSON, Type.forName(getClassName(objectJSON)));

This works when the Class is serialized in Apex, sent to the aura controller, and sent back to Apex. The Name property is populated with the value. However, when Aura is taken out of the mix, the Apex serialization/deserialization of the class that implements the method always returns NULL when deserializing into the Map<String,Object> and thus I am unable to perform the same thing in pure apex.

The class that implements the interface is as follows:

public with sharing class classThatImplements implements theInterface{
    @AuraEnabled public static string getName(){
        return 'classThatImplements';
    }

}

So, is there a way to deserialize a JSON Serialized Class into an Interface Type when one does not know the name of the class?

I know I can:

  1. simply use get; set; //Not enforceable via an interface
  2. Use an abstract class //Does not really apply as there are many classes and external integrations that will implement the interface and the Name needs to be set in the concrete class.
  3. Just tell anyone who integrates that they need to do X. //Not desirable and is essentially what 1 and 2 would require

Maybe I am not explaining it well so please do as for clarification if needed.

Best Answer

I think you should revisit the idea of using an abstract class rather than an interface. You can use the former to enforce a contract.

public class Demo
{
    final String typeName;
    public String getSomeProperty() { return typeName; }
    public abstract List<User> doStuff();
}

Granted, this definition cannot be constructed either, but deserialization into this type is allowed.

Demo instance = (Demo)JSON.deserialize('{}', Demo.class);
system.debug(instance.getSomeProperty());

And the contract gets enforced, for if you try to save a class which doesn't implement the method:

class Foo extends Demo { }

You get a compile-time error:

Class Foo must implement the abstract method: List<User> Demo.doStuff()

As a benefit, properties like typeName can be preserved throughout the serialization round-trip.


Here's a simple demonstration of that process. First define this extension:

public class Foo extends Demo
{
    final String profileName;
    public override List<User> doStuff()
    {
        return [
            SELECT Username FROM User
            WHERE Profile.Name = :profileName
        ];
    }
}

Now run this script:

String payload = '{"typeName": "Foo", "profileName": "System Administrator"}';
Map<String, Object> controllerData = (Map<String, Object>)JSON.deserializeUntyped(payload);
String controllerType = (String)controllerData.get('typeName');

Demo controller;
if (controllerType != null)
{
    controller = (Demo)JSON.deserialize(payload, Type.forName(controllerType));
    system.debug(controller.doStuff());
}

As far as why your repo works in aura but not in a pure Apex context, it looks like the system acts similarly to Visualforce, treating methods prefixed with get as properties and adding them to the serialization (since the end result is indecipherable from { get; set; } notation). If you had named your method retrieveName instead of getName, I suspect it would not have worked in aura either. You can hack around it with mimicry along the lines of:

public class MyClass
{
    final String name = getName();
    public String getName() { return 'MyClass'; }
}

But I suspect that's not a tenable long-term solution.

Related Topic