I've had to do something along these lines for an on-going integration project (but with the added bonus that the "dynamic" part of the JSON I'm getting can re-use key names but use different types for the values, e.g. "price":"5.0"
in one instance, and "price":5.0
in another)
I don't think I'd recommend the approach I had to settle on (regex to extract the "dynamic" bit of the payload with the "standardized" part of the payload containing enough information to tell me which class to use to deserialize the "dynamic" part).
General Approach
If I had to re-write the deserialization code for my integration project again, I'd probably tackle it like this:
- Deserialize the whole thing untyped to easily extract the "dynamic" bits
- Have a single class (inner class, or standalone, doesn't make too much of a difference) to do typed deserialization into for the "standardized" bits
- Do what you need to do to determine which class to "deserialize" the "dynamic" bits into
- Create a new instance of the specific class you determined you need to use for the "dynamic" bits, and pass the
Map<String, Object>
into the constructor
Taking a Map<String, Object>
and building an object instance from it can be cumbersome. If the "dynamic" bits of your JSON contain relatively little data and aren't deeply nested, I'd probably look at just passing a Map<String, Object>
into a constructor.
If your "dynamic" bits contain a lot of data and/or are deeply nested, then it might be worth it to take the penalty on CPU time to re-serialize the Map<String, Object>
of your dynamic bits and then do a typed (not necessarily strict) deserialization of the result instead of writing a ton of constructors or doing a lot of explicit type casting.
Class Structure
Let's say that our example JSON is something like this
{
"customerId":"12345",
"submittedDate":"2019-04-30 14:40:00-0500",
"orderType":"new",
"dynamicBits":{
"action":"subscribe",
"price":5.00
}
}
The "dynamicBits" don't really look all that dynamic, but let's pretend that we're using this payload structure for, say, initiating a support case as well.
"dynamicBits":"{
"createdDate":"2019-04-30 14:40:00-0500",
"supportSubject":"it isn't working :("
}
Still a rather contrived example...but it'll help illustrate things.
Each different structure of the dynamic bits could be their own class. To help things along later, you'll also need an abstract class that each of the "dynamic" subclasses extend.
public abstract class DynamicPayload{}
public class Subscription extends DynamicPayload{
public String action;
public Decimal price;
}
public class SupportTicket extends DynamicPayload{
public String supportSubject;
public DateTime createdDate;
}
The class to use for deserializing the "standardized" bits of the payload doesn't have much to talk about. It's basically a wrapper for your dynamic bits.
public class OverallPayload{
public DateTime submittedDate;
public String customerName;
// This is the most interesting bit of this class
// We don't want to have a "dynamicBits" variable name, because that would
// mean we would try to deserialize the dynamic bits (which would not end well)
// Instead, declare a variable of the abstract class's type
DynamicPayload innerPayload;
}
That abstract class we created earlier allows us to deserialize/initialize the dynamic bits and store the results in the overall payload without the need to do a bunch of type-casting wherever the deserialized data ends up being used. Some casting is still needed, but that's mostly limited to, say, Subscription explicitPayload = (Subscription)payload.innerPayload;
Applying the general approach outlined earlier
String jsonPayload = '{...}';
Map<String, Object> untyped = (Map<String, Object>)JSON.deserializeUntyped(jsonPayload);
String reSerializedDynamicBits = JSON.serialize(untyped.get('dynamicBits'));
OverallPayload deserialized = (OverallPayload)JSON.deserialize(jsonPayload, OverallPayload.class);
System.Type subclass;
// Some magic to determine which subclass to use
DynamicBits dynamicPayload = (DynamicBits)JSON.deserialize(reSerializedDynamicBits, subclass);
deserialized.innerPayload = dynamicPayload;
// and now you can send your OverallPayload instance to any method...
This approach can be helpful if you have a single REST endpoint in your org for whatever you're trying to do, and do centralized dispatch in the Apex class sitting at that endpoint.
Also helpful if you want to specify different requirements for a "valid" payload (each subclass of the abstract class can implement its own validation method)
Map<String, Object>
from that level on down.