Java Records and Jackson Custom Deserializers
I started to use Java record types and to no surprise Jackson deserialization works just like in regular POJOs. However, I came across an interesting problem to deserialize one of the fields in a record.
The JSON looks like this:
{
"team_groups": [
"1",
"2",
"5",
"6",
"8",
"60",
"95",
"10"
],
"score": 52,
"targeting": "Avoidance",
"unique_id": 918,
"children": [
"news and politics",
"politics",
"television"
],
"origin": "ADMANTS",
"name": "918§T-Mobile Direct_news politics social issues§24834",
"segment_type": "Custom",
"team": 24834,
"type": "ADMANTS",
"title": "T-Mobile Direct_news politics social issues",
"ias_code": 1500918
}
And the class/record looks like this:
public record Admant(String uniqueId,
String segmentId,
String name,
String team,
AdmantxSegmentType segmentType,
List<String> teamGroups) {
}
As you can see, all the other fields can be ignored so I started doing this with the magic Jackson annotations:
@JsonIgnoreProperties(ignoreUnknown = true)
public record Admant(@JsonProperty("unique_id") String uniqueId,
@JsonProperty("ias_code") String segmentId,
String name,
String team,
AdmantxSegmentType segmentType,
@JsonProperty("team_groups") List<String> teamGroups) {
}
Cool. Except I need to set the segmentType based on the value of "targeting". With POJOs, this is quite trivial as you can do this in the class constructor. With the Java records being immutable, this is not possible (yet). And to be quite honest this is not an elegant way to solve the problem. So I ended up using a custom deserializer, which looks something like this:
Recommended by LinkedIn
public class CustomAdmantDeserializer extends StdDeserializer<Admant> {
protected CustomAdmantDeserializer(Class<?> vc) {
super(vc);
}
protected CustomAdmantDeserializer() {
this(null);
}
@Override
public Admant deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext)
throws IOException, JacksonException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
var uniqueId = node.get("unique_id").asText();
var segmentId = node.get("ias_code").asText();
var name = node.get("name").asText();
var team = node.get("team").asText();
var teamGroups = StreamSupport
.stream(node.get("team_groups").spliterator(), false)
.map(JsonNode::asText)
.toList();
assert node.get("targeting") != null;
var segmentType = decode(node.get("targeting").asText());
return new Admant(uniqueId, segmentId, name, team, segmentType, teamGroups);
}
}
As you can see i can set the segmentType as needed, then create the record instance.
So how does the custom deserializer gets used? Either register the deserializer in the ObjectMapper like this:
var mapper = new ObjectMapper()
var module = new SimpleModule();
module.addDeserializer(Admant.class, new AdmantDeserializer());
mapper.registerModule(module);
Or the even simpler way by just using annotations, the Admant declaration looking like this:
@JsonDeserialize(using = CustomAdmantDeserializer.class)
public record Admant(String uniqueId,
String segmentId,
String name,
String team,
AdmantxSegmentType segmentType,
List<String> teamGroups) {
}
And there you have it.