[SalesForce] How to pass Apex type to method from LWC

Problem Statement

I am trying to pass an instance of an Apex type from LWC into an AuraEnabled method. However, all of the attributes are coming through as null.


MVR

Back End

public with sharing class MyController
{
    @AuraEnabled(cacheable=true)
    public static void doStuff(Model instance)
    {
        system.debug(JSON.serialize(instance));
        // { "sampleAttribute": null }
    }
    public class Model
    {
        @AuraEnabled
        public String sampleAttribute;
    }
}

Front End

import doStuff from "@salesforce/apex/MyController.doStuff"

export default class MyComponent extends LightningElement {
    ...
    const instance = {
        sampleAttribute: "I am populated"
    };
    console.debug(instance);
    // { sampleAttribute: "I am populated" }

    doStuff({ instance })
        .then(result => { ... })
        .catch(error => { ... });
}

Question

Why are my attributes all cleared out by the time they arrive at the back end? They are clearly populated right before I send them. I even tried specifying each attribute via string key just to be sure that wasn't contributing to the problem, but it did not resolve the issue.


Workaround

For now I am using serialization to get the call to work.

Back End

@AuraEnabled(cacheable=true)
public static void doStuff(String payload)
{
    doStuffWithModel((Model)JSON.deserialize(payload, Model.class));
}
public static void doStuffWithModel(Model instance)
{
    // implementation
}

Front End

const instance = {
    sampleAttribute: "I am populated"
};
doStuff({ instance })
    .then(result => { ... })
    .catch(error => { ... });

Expose Apex Methods to Lightning Web Components
To expose an Apex method to a Lightning web component, the method must be static and either global or public. Annotate the method with @AuraEnabled.

These types are supported for input and output.

  • Primitive—Boolean, Date, DateTime, Decimal, Double, Integer, Long, and String.
  • sObject—standard and custom sObjects are both supported.
  • Apex—an instance of an Apex class. (Most often a custom class.)
  • Collection—a collection of any of the other types.

But the documentation clearly indicates use of Apex types is supported. Using JSON should not be necessary.

Best Answer

This fails because the Model class is an inner class. It is probably a bug, but we hit this a while back and found:

  • instances of inner classes can be returned to the client but cannot be passed to the server
  • instances of top level classes can be passed both ways

As per sfdcfox's comment, we most often saw an internal server error when trying to pass an instance of an inner class to an aura enabled method.

The documentation does say:

Custom classes used for component attributes shouldn’t be inner classes or use inheritance. While these Apex language features might work in some situations, there are known issues, and their use is unsupported in all cases.

Whilst this appears to be for Aura code, the LWC documentation indicates LWC has the same behaviour (see the link for "Apex" in the section entitled "Expose Apex Methods to Lightning Web Components"). So whilst it is probably a bug, they have documented to say things can go wrong if you try to use inner classes.

Something else that can cause issues is the use of @AuraEnabled public attributes in your data object, which can be remedied by switching to public properties. This MAY allow you to keep the class as an inner class, but there's no guarantee this will work in all cases. Certainly changing your inner Model class to a top level one like the following will resolve any issues:

public class Model {
    @AuraEnabled
    public String someAttribute { get; set; }
}