Как десериализовать интерфейс с json-b?

Я адаптирую этот код Джексона:

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

Исходный код работает хорошо, даже для вложенных объектов Entity.

Как сделать то же самое с новой спецификацией JSON-B? Я пытался использовать @JsonbTypeDeserializer, но

  1. Это действительно путь? Кажется, не хватает простоты простого указания класса.
  2. Кажется, он не работает с вложенными объектами, что является моей самой большой проблемой:

    javax.json.bind.JsonbException: не могу вывести тип для демаршаллинга в: Entity

  3. Аннотация не подобрана для сущности. Я должен добавить вручную с помощью JsonbConfig::withDeserializers.

Вот мой код десериализатора:

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);
    }
}

Любая подсказка или помощь высоко ценится:-)

1 ответ

JSON-B не объявляет стандартный способ сериализации полиморфных типов. Но вы можете достичь этого вручную, используя специальный сериализатор и десериализатор. Я объясню это на простом примере.

Представь, что у тебя есть Shape интерфейс и два класса Square а также Circle реализуя это.

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;
    }
}

Вам необходимо сериализовать и десериализовать список, который может содержать любые Shape Реализации.

Сериализация работает из коробки:

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);

Результатом будет:

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

Это нормально, но это не сработает, если вы попытаетесь десериализовать его. Во время десериализации JSON-B необходимо создать экземпляр Square или же Circle и нет никакой информации о типе объекта в документе JSON.

Чтобы это исправить, нам нужно вручную добавить эту информацию. Здесь помогут сериализаторы и десериализаторы. Мы можем создать сериализатор, который помещает тип сериализованного объекта в документ JSON, и десериализатор, который читает его и создает надлежащий экземпляр. Это можно сделать так:

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.");
        }
    }
}

Теперь нам нужно подключить его к движку JSON-B и попробовать сериализацию. Вы не должны забывать передавать универсальный тип в движок JSON-B во время сериализации / десериализации. В противном случае это не будет работать должным образом.

// 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);

Результатом сериализации будет:

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

]

Вы видите, что тип объекта добавляется ShapeSerializer, Теперь давайте попробуем десериализовать его и напечатать результаты:

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

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

Результат:

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

Итак, это прекрасно работает. Надеюсь, поможет.:)

Ответ @Dmitry мне очень помог, но у него есть два недостатка:

1. Использование полного имени класса из JSON - серьезная проблема безопасности. Злоумышленник может заставить вас десериализовать произвольный класс, а некоторые классы могут вызвать удаленное выполнение кода. Вы должны использовать отображение (или занести разрешенные подклассы в белый список). Например:

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

2: упаковка фактического объекта в тип может не соответствовать тому, как мы хотим, чтобы наш JSON выглядел. Или, когда мы получаем JSON из другой системы, мы обычно получаем другую структуру, например, с@typeполе. И порядок полей не определен в JSON; производитель иногда может отправить@typeпоследний. Например

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

Решение, которое я нашел, таково:

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);
        }
    }
}

Обратите внимание, что чтение из Parserперемещает курсор; но нам нужно перечитать весь объект - помните:@typeне может быть первым полем. Поскольку API для сброса курсора отсутствует, я создаю новую строку JSON, вызываяtoStringи используйте это для запуска нового парсера. Это не идеально, но влияние на производительность в целом должно быть приемлемым. YMMV.

И мне не терпится увидеть полиморфный тип, поддерживаемый напрямую JSON-B, как здесь обсуждается.

Другие вопросы по тегам