I have incoming JSON data in the following format
{
"header": {
"schema_id": {
"namespace": "omh",
"name": "physical-activity",
},
},
"body": {
"activity_name": "walking",
"distance": {
"value": 1.5,
"unit": "mi"
},
}
}
and corresponding Java classes that looks like
public class DataPoint<T extends Measure> {
private DataPointHeader header;
private T body;
and
@JsonNaming(LowerCaseWithUnderscoresStrategy.class)
public class PhysicalActivity extends Measure {
private String activityName;
private LengthUnitValue distance;
I'd like Jackson to resolve body
to the PhysicalActivity
type based on the schema_id
in the JSON document, e.g. in pseudocode
if schema_id.namespace == 'omh' && schema_id.name == 'physical-activity'
then return PhysicalActivity.class
I've tried doing this with @JsonTypeIdResolver
but if I try to navigate to header.schema_id.name
with @JsonTypeInfo
, e.g.
@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "header.schema_id.name")
@JsonTypeIdResolver(DataPointTypeIdResolver.class)
public abstract class Measure {
I get a missing property: 'header.schema_id.name'
error. And even if I could, I don't think I can take a decision on both the namespace
and name
properties.
Is there a sane way to do this besides building from scratch with @JsonTypeResolver
?
@JsonSubTypes – indicates sub-types of the annotated type. @JsonTypeName – defines a logical type name to use for annotated class.
The @JsonProperty annotation is used to map property names with JSON keys during serialization and deserialization. By default, if you try to serialize a POJO, the generated JSON will have keys mapped to the fields of the POJO.
@JsonTypeInfo is used to indicate details of type information which is to be included in serialization and de-serialization.
In the Jackson source there seem to be lots of assumptions that type ids are strings, so I suspect that JsonTypeResolver is a way to go... It certainly didn't seem straightforward though!
At least for when you have just 'header' and 'body' properties, a full-custom deserializer isn't too hard:
public static class DataPointDeserializer extends StdDeserializer<DataPoint<?>> implements ResolvableDeserializer {
private JsonDeserializer<Object> headerDeserializer;
private Map<SchemaId, JsonDeserializer<Object>> activityDeserializers;
public DataPointDeserializer() {
super(DataPoint.class);
}
@Override
public void resolve(DeserializationContext ctxt) throws JsonMappingException {
headerDeserializer = ctxt.findRootValueDeserializer(ctxt.getTypeFactory().constructType(
DataPointHeader.class));
activityDeserializers = new HashMap<>();
activityDeserializers.put(new SchemaId("omh", "physical-activity"),
ctxt.findRootValueDeserializer(ctxt.getTypeFactory().constructType(PhysicalActivity.class)));
}
@Override
public DataPoint<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
String fieldName = p.nextFieldName();
if (fieldName == null)
throw ctxt.wrongTokenException(p, JsonToken.FIELD_NAME, "expected 'header' and 'body' fields");
if (fieldName.equals("header")) {
p.nextToken();
DataPointHeader header = (DataPointHeader) headerDeserializer.deserialize(p, ctxt);
JsonDeserializer<Object> bodyDeserializer = activityDeserializers.get(header.schemaId);
if (bodyDeserializer == null) throw ctxt.mappingException("No mapping for schema: " + header.schemaId);
fieldName = p.nextFieldName();
if (fieldName == null)
throw ctxt.wrongTokenException(p, JsonToken.FIELD_NAME, "expected 'body' field after header");
p.nextToken();
Measure body = (Measure) bodyDeserializer.deserialize(p, ctxt);
DataPoint<Measure> dataPoint = new DataPoint<>();
dataPoint.header = header;
dataPoint.body = body;
return dataPoint;
}
else if (fieldName.equals("body")) {
p.nextToken();
try (TokenBuffer tb = new TokenBuffer(p)) {
tb.copyCurrentStructure(p);
fieldName = p.nextFieldName();
if (fieldName == null)
throw ctxt.wrongTokenException(p, JsonToken.FIELD_NAME, "expected 'header' field after body");
if (!fieldName.equals("header"))
throw ctxt.weirdStringException(fieldName, DataPoint.class, "Unexpected field name");
p.nextToken();
DataPointHeader header = (DataPointHeader) headerDeserializer.deserialize(p, ctxt);
JsonDeserializer<Object> bodyDeserializer = activityDeserializers.get(header.schemaId);
if (bodyDeserializer == null)
throw ctxt.mappingException("No mapping for schema: " + header.schemaId);
JsonParser bodyParser = tb.asParser();
bodyParser.nextToken();
Measure body = (Measure) bodyDeserializer.deserialize(bodyParser, ctxt);
DataPoint<Measure> dataPoint = new DataPoint<>();
dataPoint.header = header;
dataPoint.body = body;
return dataPoint;
}
}
else throw ctxt.weirdStringException(fieldName, DataPoint.class, "Unexpected field name");
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With