I have a standard polymorphic type Shape, and I can polymorphically deserialise it using the standard @JsonTypeInfo mechanism:
import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
public class DiscriminatorAliasTest {
@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Square.class, name = "square"),
@JsonSubTypes.Type(value = Circle.class, name = "circle")
})
static abstract class Shape {
abstract public String name();
}
static class Square extends Shape {
@JsonProperty("A")
public float width;
@Override
public String name() { return "square"; }
}
static class Circle extends Shape {
@JsonProperty("A")
public float diameter;
@Override
public String name() { return "circle"; }
}
@Test
public void testDiscriminator() throws Exception {
ObjectMapper mapper = new ObjectMapper();
// This passes!
String squareJson = "{\"type\":\"square\", \"A\": 1.0 }";
Shape square = mapper.readerFor(Shape.class).readValue(squareJson);
assertThat(square.name()).isEqualTo("square");
}
}
However, I would like to add ? as an alias to the discriminator property type such that the following JSON strings are deserialised by the Shape abstract class:
{ "type": "square", "A": 1.0 } AND { "?": "square", "A": 1.0 }
such that this test case would pass
@Test
public void testDiscriminator() throws Exception {
ObjectMapper mapper = new ObjectMapper();
// This passes!
String squareJson = "{\"type\":\"square\", \"A\": 1.0 }";
Shape square = mapper.readerFor(Shape.class).readValue(squareJson);
assertThat(square.name()).isEqualTo("square");
// and this should pass as well, with NO further modification
// to this test case
String squareJsonWithAlternativePropertyName = "{\"?\":\"square\", \"A\": 1.0 }";
Shape alsoSquare = mapper.readerFor(Shape.class).readValue(squareJsonWithAlternativePropertyName);
assertThat(alsoSquare.name()).isEqualTo("square");
}
Why am I trying to do this?: I am trying to deserialise two different existing discriminated union encoding schemes onto the same class hierarchy - unfortunately there is no way in advance to know which scheme the class must deserialise.
Restrictions:
@JsonTypeInfo(use = Id.DEDUCTION). There must be a discriminator property because the property key set cannot be used to disambiguate each class.ObjectMapper prior to mapping as this pattern exposes deserialisation implementation details to external modules. I assume this might rule out module.setDeserializerModifier / DelegatingDeserializer / BeanDeserializerModifier based solutions, such as the ones suggested in this question and this question.JsonNode structure before it hits the ObjectMapperShape instance by hand by destructuring JsonNodes. I should reasonably expect to utilise the existing deserialisation machinery and annotations of Shape and its subtypes.I am assuming these are reasonable restrictions - setting an alias for the discriminator property in Scala's Circe JSON library whilst complying with these restrictions was comparatively a single line change.
Thanks in advance!
Here is a way to solve your problem. The idea is to define a custom deserializer and associate it with the base class Shape, but
restore the default deserializer for the inherited classes (via an empty @JsonDeserialize annotation).
That trick prevents an endless loop.
The deserializer works like this:
treeToValue() with the tree and the typeimport java.io.IOException;
import java.util.HashMap;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.node.*;
public class Test
{
@JsonDeserialize(using = ShapeDeserializer.class)
public static abstract class Shape
{
abstract public String name();
}
@JsonDeserialize
public static class Square extends Shape
{
@JsonProperty("A")
public float width;
@Override
public String name() { return "square"; }
}
@JsonDeserialize
public static class Circle extends Shape
{
@JsonProperty("A")
public float diameter;
@Override
public String name() { return "circle"; }
}
static class ShapeDeserializer extends JsonDeserializer<Shape>
{
private HashMap<String, Class<? extends Shape>> types = new HashMap<>();
public ShapeDeserializer()
{
types.put("square", Square.class);
types.put("circle", Circle.class);
}
public Shape deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
// Get the tree and extract the type from it
ObjectNode root = (ObjectNode)p.readValueAsTree();
TextNode type = (TextNode)root.remove("type");
if(type==null)
type = (TextNode)root.remove("?");
Class<? extends Shape> valueType = types.get(type.textValue());
// Convert the tree to an instance of the specified class
return p.getCodec().treeToValue(root, valueType);
}
}
public static void main(String[] args) throws JsonProcessingException
{
String squareJson = "{\"type\":\"square\", \"A\": 1.0 }";
String circleJson = "{\"?\":\"circle\", \"A\": 1.0 }";
ObjectMapper mapper = new ObjectMapper();
Shape square = mapper.readerFor(Shape.class).readValue(squareJson);
Shape circle = mapper.readerFor(Shape.class).readValue(circleJson);
System.out.println(square.name());
System.out.println(circle.name());
}
}
Output:
square
circle
There is no jackson builtin mechanism that can help you to achieve your goal, a workaround is convert the json string to a ObjectNode, check if the "?" property is present in it, if so add the "type" property with the "?" associated value, delete the "?" property and use the ObjectMapper#treeToValue method like below:
String squareJsonWithAlternativePropertyName = "{\"?\":\"square\", \"A\": 1.0 }";
ObjectNode node = (ObjectNode) mapper.readTree(squareJsonWithAlternativePropertyName);
if (node.has("?")) {
node.set("type", node.get("?"));
node.remove("?");
}
Shape shape = mapper.treeToValue(node, Shape.class);
You can encapsulate this code in one function, in case of nested properties or another case the code has to be updated but substantially the basecode remains still the same.
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