I have a service returning JSON with a "stable" response, but containing "projection" field which can vary. Example:
{
"name": "test",
"projection": {
"id": 1,
"description": "Hey there",
"blurb": "This is a test"
}
}
I want to deserialize into records and provide a mechanism where one can define their own record for the projection that has been returned. This is what I came up with, a Response record with a generic member that implements a Projection interface.
package com.example.json
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import java.io.IOException;
public class Example {
private static final ObjectMapper MAPPER =
new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, true)
.registerModule(new Jdk8Module());
private static final String SAMPLE_JSON =
"{\"name\":\"test\", \"projection\": {\"id\": 1, \"description\": \"Hey there\", \"blurb\": \"This is a test\"}}";
record Response<T extends Projection>(String name, T projection) {}
interface Projection {
int id();
static <T extends Projection> Response<T> parse(byte[] bytes) {
try {
TypeReference<Response<T>> typeReference = new TypeReference<>() {};
return MAPPER.readValue(bytes, typeReference); // Explodes here
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
record Projection1(int id, String description, String blurb) implements Projection {}
public static void main(String[] args) {
Response<Projection1> response = Projection.<Projection1>parse(SAMPLE_JSON.getBytes());
System.out.println(response);
}
}
This fails with an error:
Exception in thread "main" java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.json.Example$Projection` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 31] (through reference chain: com.example.json.Example$Response["projection"])
at com.example.json.Example$Projection.parse(Example.java:29)
at com.example.json.Example.main(Example.java:37)
I'm foreseeing quite a few variations of the projections. Is there a way for me to keep the parsing logic in the interface / one place, and avoid having to e.g. implement the parse method on each concrete Projection implementation, which would have access to the concrete types?
Got this to work by adding the
TypeReferenceto theProjection::parsesignature.And then pass it when calling:
Ideally, I wouldn't want to pass the
TypeReference, but this works.