Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deserialize an interface with json-b?

I'm adapting this Jackson code:

@JsonDeserialize(as = EntityImpl.class)
public interface Entity { ... }

The original code works well, even for nested Entity objects.

How to do the same with the new json-b specification? I tried using @JsonbTypeDeserializer but

  1. Is that really the way to go? It seems to lack the simplicity of just specifying a class.
  2. It doesn't seem to work with nested entities, which is my biggest problem:

    javax.json.bind.JsonbException: Can't infer a type for unmarshalling into: Entity

  3. The annotation is not picked up on Entity. I have to add manually with JsonbConfig::withDeserializers.

Here is my deserializer code:

public class EntityDeserializer implements JsonbDeserializer<Entity> {

    @Override
    public Entity deserialize(JsonParser parser, DeserializationContextdeserializationContext, Type runtimeType) {
        Class<? extends Entity> entityClass = EntityImpl.class.asSubclass(Entity.class);
        return deserializationContext.deserialize(entityClass, parser);
    }
}

Any hint or help greatly appreciated :-)

like image 678
David Avatar asked Sep 05 '17 08:09

David


2 Answers

JSON-B doesn't declare a standard way of serializing polymorphic types. But you can manually achieve it using custom serializer and deserializer. I'll explain it on a simple sample.

Imagine that you have Shape interface and two classes Square and Circle implementing it.

public interface Shape {
    double surface();
    double perimeter();
}

public static class Square implements Shape {
    private double side;

    public Square() {
    }

    public Square(double side) {
        this.side = side;
    }

    public double getSide() {
        return side;
    }

    public void setSide(double side) {
        this.side = side;
    }

    @Override
    public String toString() {
        return String.format("Square[side=%s]", side);
    }

    @Override
    public double surface() {
        return side * side;
    }

    @Override
    public double perimeter() {
        return 4 * side;
    }
}

public static class Circle implements Shape {
    private double radius;

    public Circle() {
    }

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }

    @Override
    public String toString() {
        return String.format("Circle[radius=%s]", radius);
    }

    @Override
    public double surface() {
        return Math.PI * radius * radius;
    }

    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }
}

You need to serialize and deserialize List which can contain any Shape implementations.

Serialization works out of the box:

JsonbConfig config = new JsonbConfig().withFormatting(true);
Jsonb jsonb = JsonbBuilder.create(config);

// Create a sample list
List<SerializerSample.Shape> shapes = Arrays.asList(
            new SerializerSample.Square(2),
            new SerializerSample.Circle(5));

// Serialize
String json = jsonb.toJson(shapes);
System.out.println(json);

The result will be:

[
    {
        "side": 2.0
    },
    {
        "radius": 5.0
    }
]

It's ok, but it won't work if you try to deserialize it. During deserialization JSON-B needs to create an instance of Square or Circle and there is no information about object type in the JSON document.

In order to fix it we need to manually add this information there. Here serializers and deserializers will help. We can create a serializer which puts a type of serialized object in JSON document and deserializer which reads it and creates a proper instance. It can be done like this:

public static class ShapeSerializer implements JsonbSerializer<SerializerSample.Shape> {
    @Override
    public void serialize(SerializerSample.Shape shape, JsonGenerator generator, SerializationContext ctx) {
        generator.writeStartObject();
        ctx.serialize(shape.getClass().getName(), shape, generator);
        generator.writeEnd();
    }
}

public static class ShapeDeserializer implements JsonbDeserializer<SerializerSample.Shape> {
    @Override
    public SerializerSample.Shape deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
        parser.next();

        String className = parser.getString();
        parser.next();

        try {
            return ctx.deserialize(Class.forName(className).asSubclass(Shape.class), parser);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new JsonbException("Cannot deserialize object.");
        }
    }
}

Now we need to plug it into JSON-B engine and try serialization. You should not forget to pass a generic type to JSON-B engine during serialization/deserialization. Otherwise it won't work properly.

// Create JSONB engine with pretty output and custom serializer/deserializer
JsonbConfig config = new JsonbConfig()
        .withFormatting(true)
        .withSerializers(new SerializerSample.ShapeSerializer())
        .withDeserializers(new SerializerSample.ShapeDeserializer());
Jsonb jsonb = JsonbBuilder.create(config);

// Create a sample list
List<SerializerSample.Shape> shapes = Arrays.asList(
        new SerializerSample.Square(2),
        new SerializerSample.Circle(5));

// Type of our list
Type type = new ArrayList<SerializerSample.Shape>() {}.getClass().getGenericSuperclass();

// Serialize
System.out.println("Serialization:");
String json = jsonb.toJson(shapes);
System.out.println(json);

The result of serialization will be:

[
    {
        "jsonb.sample.SerializerSample$Square": {
            "side": 2.0
        }
    },
    {
        "jsonb.sample.SerializerSample$Circle": {
            "radius": 5.0
        }
    }

]

You see that object type is added by ShapeSerializer. Now let's try to deserialize it and print results:

// Deserialize
List<SerializerSample.Shape> deserializedShapes = jsonb.fromJson(json, type);

// Print results
System.out.println("Deserialization:");
for (SerializerSample.Shape shape : deserializedShapes) {
    System.out.println(shape);
}

The result is:

Square[side=2.0]
Circle[radius=5.0]

So, it perfectly works. Hope it helps. :)

like image 190
Dmitry Avatar answered Sep 28 '22 18:09

Dmitry


Edit: JSON-B 3.0 supports Polymorphic Types directly. See here.

The answer @Dmitry gave helped me a lot, but it has two flaws:

1: using the full class name from the JSON is a serious security issue. An attacker could make you deserialize an arbitrary class, and some classes can cause remote code execution. You must use a mapping (or whitelist the allowed subclasses). E.g.:

[
    {
        "square": {
            "side": 2.0
        }
    },
    {
        "circle": {
            "radius": 5.0
        }
    }
]

2: wrapping the actual object within the type may not be what we want our JSON to look like. Or when we receive the JSON from a different system, we commonly get a different structure, e.g. with a @type field. And the field order is not defined in JSON; a producer might sometimes send the @type last. E.g.

[
    {
        "@type":"square",
        "side": 2.0
    },
    {
        "radius": 5.0,
        "@type":"circle"
    }
]

The solution I found is this:

public class ShapeDeserializer implements JsonbDeserializer<Shape> {
    @Override public Shape deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
        JsonObject value = parser.getObject();
        String type = value.getString("@type", "null");
        return JSONB.fromJson(value.toString(), classFor(type));
    }

    private Class<? extends Shape> classFor(String type) {
        switch (type) {
            case "circle":
                return Circle.class;
            case "square":
                return Square.class;
            default:
                throw new JsonbException("unknown shape type " + type);
        }
    }
}

Note that reading from the Parser advances the cursor; but we need to re-read the complete object – remember: the @type may not be the first field. As there is no API to reset the cursor, I produce a new JSON string by calling toString and use that to start a new parser. This is not perfect, but the performance impact should be generally acceptable. YMMV.

And I'm eager to see polymorphic type supported directly by JSON-B as discussed here.

like image 34
rü- Avatar answered Sep 28 '22 16:09

rü-