Reputation: 113
We are building an app that may produce hundreds of unique JSON payload structures from our streamlined object model and we want to avoid adding hundreds of POJOs to our Java codebase (1 per unique payload structure).
We built a way to build, parse and overlay a simple string spec of the payload structure to a POJO and to walk its tree to match any @JsonProperty
fields. Here's an example spec:
String spec = """
firstName
lastName
addresses.line1
addresses.city
children.firstName
children.schedule.bedtime
"""
This would be overlaid on a Person
POJO at runtime and it would traverse and get the fields and arrays specified. Though the Person
, Child
and Address
POJOs have plenty more fields in them, this specific message request should only populate the ones we specified. The JSON output of this should be:
{
"firstName": "Mickey",
"lastName": "Mouse",
"addresses": [
{
"line1": "123 Disneyland Way",
"city": "Anaheim"
},
{
"line2": "345 Disneyworld Drive",
"city": "Orlando"
}
],
"children": [
"firstName": "Junior",
"schedule": [
"bedtime": "20:00"
]
]
}
bear with me here...seems like only 1 Disney character has any children (Ariel)
Currently, we have only tested our Specs and the traversal of POJOs to find where the Spec applies. But we cannot figure out how to wire this into the JSON serialization process.
Everything I've read about Jackson/JSON/Spring serializers and deserializers seem to only take 1 input - the POJO. Is there a way to use custom serializers that use 2 inputs (POJO + Spec), where Spec is identified at runtime (thus eliminating the need to create a "wrapper" POJO per and/or serializer per Spec)?
To make things even more challenging, we want to simplify our @RestController
methods to simply include our new annotation, @APIMessageSpec(<details go here>)
alongside the @ResponseBody
annotation and then have Spring invoke our custom serialization process, passing in the POJO and details of our @APIMessageSpec
(or possibly for us to subclass @ResponseBody
to parameterize the Spec info in its arguments, so that we don't need 2 annotations)?.
Thanks in advance! Michael
Upvotes: 2
Views: 988
Reputation: 113
I figured out a solution not long after posting this!
All of our data/object model POJOs share a superclass in their ancestry, BaseModelEntity
(where we put some common fields like UUID, last update user/timestamp, etc.)
We created ModelJsonSerializer extends JsonSerializer<BaseModelEntity>
for our custom serialization rules.
We picked a bean with @Autowired ObjectMapper objectMapper
to add the following method:
@PostConstruct
public void init() {
SimpleModule simpleModule = new SimpleModule("OurCustomModelJsonModule", new Version(1, 0, 0, null, null, null));
simpleModule.addSerializer(BaseModelEntity.class, new ModelJsonSerializer());
objectMapper.registerModule(simpleModule);
}
We have a static APISpecRegistry
class where we load all of our specs [which all subclass our custom APISpec
superclass] in a HashMap.
On our @RestController
's methods, or anywhere in our application for that matter, we annotate the return type with @JsonView(<our_Spec_class>)
When Jackson identifies and starts to process any of our data model objects it successfully finds ModelJsonSerializer
as the correct one for our data type BaseModelEntity
.
In ModelJsonSerializer
, we have the following method:
@Override
public void serialize(BaseModelEntity value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
Class c = serializers.getActiveView();
if(c != null && APISpec.class.isAssignableFrom(c)) {
List<Field> fields = ((APISpec)APISpecRegistry.getSpec(c).get()).getFields();
serialize(value, gen, serializers, fields);
} else {
// default to out-of-the-box JSON serializers
}
}
NOTE: APISpec.getFields()
is where we parsed our spec from string format to a hierarchy of POJO fields to traverse.
We get the current @JsonView
via serializers.getActiveView()
, get our APISpec
class from that view and use that as the key to lookup the APISpec
object from our APISpecRegistry
Then, in our custom 4-argument serialize
method, we cross-reference our API rules for each field with the corresponding field values from our BaseModelEntity
instance and ignore any BaseModelEntity
field that's not in our Spec.
TIP: Underneath the covers, we use Java reflection to find java.lang.reflect.Field
s with a @JsonProperty
annotation that matches up to the field in our APISpec
. So, you can image, we have the following fields in our Person
object:
@JsonProperty("firstName")
private String fName;
@JsonProperty("lastName")
private String lName;
@JsonProperty("addresses")
List<Address> addresses;
@JsonProperty("children")
List<Child> kiddos;
It's MAGIC!
Upvotes: 1