Как десериализовать интерфейс с json-b?
Я адаптирую этот код Джексона:
@JsonDeserialize(as = EntityImpl.class)
public interface Entity { ... }
Исходный код работает хорошо, даже для вложенных объектов Entity.
Как сделать то же самое с новой спецификацией JSON-B? Я пытался использовать @JsonbTypeDeserializer, но
- Это действительно путь? Кажется, не хватает простоты простого указания класса.
- Кажется, он не работает с вложенными объектами, что является моей самой большой проблемой:
javax.json.bind.JsonbException: не могу вывести тип для демаршаллинга в: Entity
- Аннотация не подобрана для сущности. Я должен добавить вручную с помощью 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, как здесь обсуждается.