4

I'm trying to deserialize an API response into Apex classes using JSON.deserialize. Everything works fine, until I try to customize a setter on a list property. I've distilled this down to an anonymous Apex reproduction case.

class Child {
    String name {get; set;}
}

class Parent {
    String name {get; set;}
    List<Child> children {
        get; 
        set {
            children = value;
            this.childCount = children.size();
        }
    }
    Integer childCount {get; set;}
}
String jsonStr = '{"name": "Alice", "children": [{"name": "Bobby"},{"name": "Charlie"}]}';
Parent p = (Parent)JSON.deserialize(jsonStr, Parent.class);
System.debug('p.childCount: ' + p.childCount);
System.debug('p: ' + JSON.serialize(p));
System.debug('p.children.size(): ' + p.children.size());

This produces the debug output:

|DEBUG|p.childCount: 0
|DEBUG|p: {"name":"Alice","children":[{"name":"Bobby"},{"name":"Charie"}],"childCount":0}
|DEBUG|p.children.size(): 2

Note that childCount is 0, but children is set correctly. I can even get the correct count after the deserialize is complete. That doesn't make sense, so I added some debugging to my setter:

        set {
            System.debug('value: ' + JSON.serialize(value));
            children = value;
            System.debug('children.size(): ' + children.size());
            System.debug('children.clone().size(): ' + children.clone().size());
            this.childCount = children.size();
        }

this adds the debug output:

|DEBUG|value: []
|DEBUG|children.size(): 0
|DEBUG|children.clone().size(): 0

It appears that value is empty? And even if value has some special compiler-magic that prevents it from being inspected, children is also empty after the assignment. But I get the debugger output, so I know my setter is being called.

Finally, I tried using a new List to avoid any "specialness" around value:

        set {
            children = new List<Child>();
            children.addAll(value);
            System.debug('children.size(): ' + children.size());
            this.childCount = children.size();
        }

And now I don't even get the children:

|DEBUG|children.size(): 0
|DEBUG|p.childCount: 0
|DEBUG|p: {"name":"Alice","children":[],"childCount":0}
|DEBUG|p.children.size(): 0

I have also tried writing an explicit getter, and using "this.children" instead of "children" in the setter, with no difference in results. What is happening?

Why do I care? The actual object model is much deeper, and the real "Parent" object is well down the hierarchy, and may have anywhere from 1 to thousands of children. I was trying to add a simple setter that captures the list size ("childCount"), and then clears the list if it has more that 10 members. I could iterate the whole deserialized structure, but a setter seems like a more efficient way to take action based on the contents of the property.

2
  • My impression is that the deserialization implementation works close to the byte by byte level e.g. any constructor isn't called. Never looked at setters but perhaps similarly the code is side stepped though of course that doesn't explain your later debug logs.
    – Keith C
    Commented Mar 23, 2022 at 19:23
  • In addition, unless the setter is actually intended to do something other than the trivial side-effect of capturing an array size at the time the children list is set, as shown in this question, I'm not sure I really understand why you would want to write the code this way.
    – Phil W
    Commented Mar 23, 2022 at 20:39

1 Answer 1

8

JSON deserialization (through JSON.deserialize()) creates object instances in a way that's different from how objects are created in the rest of Apex1. I don't think that's really relevant in this case, but it's something I like to keep in mind.

Instead, I think the issue here is that the setter for List<Child> children is only run when the list is set and not when the list has items added to it (and it's being set while the list is still empty).

Given that you are seeing the debugs you placed into the setter, it seems reasonable to say that JSON.deserialize() is:

  • Internally keeping an instance of a List<Child> (initially empty)
  • Setting the intermediate Parent instance's children to that empty list
    • Which calls the setter
  • When you have the setter use its own new instance of List<Child>, you're breaking the reference to the internally maintained instance
    • So JSON.deserialize() is still adding children to its internal list instance, but it's not ending up in your completed object

1: One of the big differences is that constructors are not executed

1
  • That has to be it. Invalidates my approach, but at least I understand why now. Commented Mar 23, 2022 at 19:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .