[SalesForce] Aura vs Apex – Deserialization and Interface discrepancy (Major rewrite with git repo)

Question rewritten and complete example code provided

I am trying to write test methods for an aura implementation but am running into a roadblock where the JSON serialization/deserialization between Aura and Apex are producing different results.

I am curious to know if my aura implementation is wrong of if there is something else I am doing wrong.

The interface

public interface theInterface {
    void anImplementedMethod();
    String getName();
}

The Class that implements said interface

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

    @AuraEnabled public String somethingPreset {get { return 'preset'; }set;}

    public static void anImplementedMethod(){
        System.debug('I am doing something');
    }

}

The Lightning Component

<aura:component description="testComponent" implements="force:hasRecordId" controller="auraController">

    <aura:handler name="init" value="{!this}" action="{!c.init}"/>
    <aura:attribute name="returnedObject" type="theInterface" description="the returned interface implementation"/>
    <aura:attribute name="result" type="String"/>


    <lightning:button variant="brand" label="Do it" onclick="{!c.callController}"/>

    {!v.result}
</aura:component>

The Lightning Controller

({
    init: function (component, event, helper) {

        var response = component.get("c.getClassThatImplements");

        response.setCallback(this, function (returnValue) {
            if (component.isValid() && returnValue.getState() === 'SUCCESS') {
                var returnedData = returnValue.getReturnValue();

                //This will show that the name property has been set
                console.log(JSON.stringify(returnedData));

                component.set("v.returnedObject", returnedData);


            } else if (component.isValid() && returnValue.getState() === 'ERROR') {
                alert(returnValue.getError()[0].message);
            }

        });
        $A.enqueueAction(response);

    },
    callController: function(component, event, helper){
        var doIt = component.get("c.consumeClassThatImplements");

        doIt.setParams({
            "objectJSON": JSON.stringify(component.get("v.returnedObject"))
        });

        //Do the initial update to the profile to ready for Chargent Use
        doIt.setCallback(this, function (rtn) {
            if (component.isValid() && rtn.getState() === 'SUCCESS') {
                component.set("v.result",rtn.getReturnValue());
            } else if (component.isValid() && rtn.getState() === 'ERROR') {
                alert(rtn.getError()[0].message);
            }

        });
        $A.enqueueAction(doIt);

    },

})

During init of the aura component I call to the controller and get the results. The results are output to the console and assigned to the attribute..

Console OutputNOTE: name is populated

{"name":"classThatImplements","somethingPreset":"preset"}

This is what I want, seems like it is using the aura enabled property to get the name and populate it as a property.

Now comes the apex part

Apex Controller

public class auraController {

    /**
     * This will return a class that implements the interface.
     * It is meant to determine which class to instantiate as there would be several classes that implement the interface
     * Which one is determined by the record type of an object that would be passed into this method
     *
     * @return Object A generic Object that can be return to Aura.
     */
    @AuraEnabled public static Object getClassThatImplements() {
        try {
            return (theInterface)New classThatImplements();
        }catch(Exception e){
            AuraHandledException aex = New AuraHandledException(e.getMessage());
            aex.setMessage(e.getMessage());
            throw aex;
        }

    }

    /**
     * Takes the JSON representation of the implemented Interface Class
     *
     * @param objectJSON
     *
     * @return
     */
    @AuraEnabled public static String consumeClassThatImplements(String objectJSON) {
        try {
            //Since we cannot use an interface for the Apex Type need to figure out the class name and deserialze casting to interface type
            theInterface controller = (theInterface) json.deserialize(objectJSON, Type.forName(getClassName(objectJSON)));
            controller.anImplementedMethod();
            return 'Look ma no errors';
        }catch(Exception e){
//            We Don't expect any aura errors for this demo
//            AuraHandledException aex = New AuraHandledException(e.getMessage());
//            aex.setMessage(e.getMessage());
//            throw aex;
            throw new auraController_Exception(e.getMessage()); //So we actually get a message in the apex demo
        }
    }

    /**
     * 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;


    }

    public class auraController_Exception extends Exception{}

}

From the Aura component I pass the JSON of the attribute to the method consumeClassThatImplements which then deserializes it. In order to do so we need to know the concrete class name, not the interface so the helper method getClassName is called to get the name (remember in aura is was set and output so we know we have it in the JSON)

Doing this during the Aura lifecycle works just fine

Apex Failure

Running the same code path in Apex during tests always returns NULL for the name when I try to get it. Here is the code to use to show that doing the same thing (minus the Apex <=> Aura communication)

String tmp = json.serialize(auraController.getClassThatImplements());
System.debug(auraController.consumeClassThatImplements(tmp));

The above results in:

Error on line 41, column 1: auraController.auraController_Exception: Well, this did not work as the name property was blank

So my question is:

  1. Why does the Name property get populated in Aura during deserialization from Apex and assignment to the attribute – I am glad it does
  2. Why during apex outside of Aura does the Name property always return null?

Is one a bug, are they both working as expected?

I took inspiration from here: Cannot deserialize JSON as abstract type?

But the fact that during Apex tests it does not behave as it does in aura concerns me.

I have shared the project on GitHub so all you need to do to test is:

  1. Create a scratch org
  2. Clone the repo and push to scratch org
  3. Open scratch org
  4. hit /apex/vfTestPage
  5. look at the console and verify that name is present
  6. Click "Do It" to call the Apex Controller method
  7. Observe that "Look ma no errors" is presented (successfully ran)

Then

In dev console execute

String tmp = json.serialize(auraController.getClassThatImplements());
System.debug(auraController.consumeClassThatImplements(tmp));

An you will get an error thrown which will show that in apex only JSON serialization.deserialization the name property is not available

Best Answer

There are four different things that work different and should not be mixed up:

  1. Attributes visible to Apex (System.debug(object))
  2. Attributes visible to JSON.serialize
  3. Attributes visible to Aura
  4. Attributes visible to VisualForce

At the end of this post, you will find the class with different types of attributes (public; getter; getMethod; auraEnabled; transient). But first I want to point out the differences between these four possible outputs.

1) Apex Debug: Only declared attributes are printed. getAttribute methods aren't visible if we debug the object itself, not even {get{return x;}} is executed (null). They only get executed, when we access them directly.

Thing:[auraEnable=x, getter=null, publc=x, transi=x]

2) JSON: JSON.serialize uses the same information, so it only sees declared attributes, but also knows how to call their getters get{return x;}, so we are one step further. (You can use transient to hide it from JSON).

{"publc":"x","getter":"x","auraEnable":"x"}

3) Aura: Aura sees everything, that is @auraEnabled and knows how to call the getters to get its information. It is not using JSON.serialize()!

Object { method: "x", auraEnable: "x", getter: "x" }

4) Visualforce: sees all attributes including transient and (oddly) also auraEnabled (but doesn't know how to execute getters without direct calls.) In fact VF sees everything that is also outputted in debug(thing);

Thing:[auraEnable=x, getter=null, publc=x, transi=x] 

The Answer:

Aura seems to be the smartest of all these possibilities to output an object. It is the only one that executed both getter methods. JSON.serialize only executed the public get{}.

So in your case, I would suggest replacing your interface with an abstract class using the template method pattern:

public abstract class theInterface{
    public String className { public get{ return getClassName(); } set; }

    public abstract String getClassName();
}

public class ImplementingThing extends theInterface{
    @auraEnabled
    public override String getClassName() {
        return ImplementingThing.class.getName();
    }
}

This would work in Aura and in your Test. Be aware that there are some issues about abstract classes and their properties in Lightning. Here are some more ways to get the classes name.


This is the code to verify all this:

public class ObjectCtrl {

    @auraEnabled
    public static Thing getThing() {
        Thing result = new Thing();

        System.debug(result);
        System.debug(JSON.serialize(result));

        return result;
    }

    public class Thing {
        public String publc = 'x';

        @auraEnabled
        public String getter { public get{ return 'x'; } set; }

        @auraEnabled
        public String getMethod() {
            return 'x';
        }

        @auraEnabled
        public String auraEnable = 'x';

        transient
        public String transi = 'x';
    }
}

Visual Force:

<apex:page controller="ObjectCtrl">
    {!thing}
</apex:page>

Lightning App (Aura):

<aura:application controller="ObjectCtrl">
    <aura:handler name="init" value="{!this}" action="{!c.onInit}"/>
</aura:application>

({
    onInit: function(cmp, evt, helper) {
        var action = cmp.get("c.getThing");

        action.setCallback(this, function(response) {
            console.log(response.getReturnValue());
        });

        $A.enqueueAction(action);
    },
})
Related Topic