Пользовательский десериализатор Jackson для полиморфных c объектов и строковых литералов по умолчанию - PullRequest
1 голос
/ 25 февраля 2020

Я бы хотел десериализовать объект из YAML со следующими свойствами, используя Джексона в приложении Spring Boot:

  • Абстрактный класс Vehicle, реализованный Boat и Car
  • Для простоты представьте, что у обоих есть имя, но только у Boat есть также свойство seaworthy, а у Car есть top-speed.
mode-of-transport:
  type: boat
  name: 'SS Boatface'
  seaworthy: true
----
mode-of-transport:
  type: car`
  name: 'KITT'
  top-speed: 123

Все это прекрасно работает в моих аннотированных подклассах, используя @JsonTypeInfo и @JsonSubTypes!

Теперь я хотел бы создать сокращение, используя только строковое значение, которое должно создать Car по умолчанию с таким именем:

mode-of-transport: 'KITT'

Я пытался создать свой собственный сериализатор, но застрял на большинстве важных деталей. Пожалуйста, помогите мне заполнить это, если это правильный подход:

public class VehicleDeserializer extends StdDeserializer<Merger> {

   /* Constructors here */

   @Override
   public Vehicle deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
      if (/* it is an OBJECT */){
         // Use the default polymorphic deserializer 
      } else if (/* it is a STRING */) {
         Car car = new Car();
         car.setName( /* the String value */ );
         return car;
      }
      return ???; /* what to return here? */
   }
}

Я нашел эти 2 ответа для вдохновения, но похоже, что объединение их с полиморфными типами c усложняет: Как вызвать десериализатор по умолчанию из пользовательского десериализатора в Джексоне и Десериализацию в строку или объект с использованием Джексона

Несколько вещей отличаются от решений, предлагаемых в этих вопросах:

  • Я обрабатываю YAML, а не JSON. Не уверен насчет тонких различий.
  • У меня нет проблем с жестким программированием типа «по умолчанию» для строк в моем десериализаторе, надеюсь, что это будет проще.

Ответы [ 2 ]

1 голос
/ 02 марта 2020

Это было на самом деле проще, чем я думал, чтобы решить это. Я получил его с помощью следующего:

  1. Пользовательская реализация десериализатора:
public class VehicleDeserializer extends StdDeserializer<Vehicle> {

    public VehicleDeserializer() {
        super(Vehicle.class);
    }

    @Override
    public Vehicle deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        if (jp.currentToken() == JsonToken.VALUE_STRING) {
            Car car = new Car();
            car.setName(jp.readValueAs(String.class));
            return car;
        }
        return jp.readValueAs(Vehicle.class);
    }
}
Чтобы избежать циклических зависимостей и заставить настраиваемый десериализатор работать с аннотациями polymorphi c @JsonTypeInfo и @JsonSubTypes, я сохранил эти аннотации на уровне класса Vehicle, но поместил следующие аннотации в объект контейнера Я десериализирую:
public class Transport {

    @JsonDeserialize(using = VehicleDeserializer.class)
    @JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
    private Vehicle modeOfTransport;

    // Getter, setters
}

Это означает, что по умолчанию Транспортное средство десериализуется как объект polymorphi c, если явно не указано, чтобы десериализовать его с помощью моего пользовательского десериализатора. Этот десериализатор, в свою очередь, будет в свою очередь откладывать полиморфизм, если входные данные не являются String.

Надеемся, это поможет кому-то столкнуться с этой проблемой:)

0 голосов
/ 26 февраля 2020

Таким образом, существует решение, которое требует, чтобы вы обрабатывали ошибки Джексона, используя DeserializationProblemHandler (так как вы хотите анализировать один и тот же тип, используя разные входы, это не легко достигается с помощью обычных средств):

public class MyTest {

    @Test
    public void doTest() throws JsonParseException, JsonMappingException, IOException {
        final ObjectMapper om = new ObjectMapper();
        om.addHandler(new DeserializationProblemHandler() {

            @Override
            public Object handleMissingInstantiator(final DeserializationContext ctxt, final Class<?> instClass, final JsonParser p, final String msg) throws IOException {
                if (instClass.equals(Car.class)) {
                    final JsonParser parser = ctxt.getParser();
                    final String text = parser.getText();
                    switch (text) {
                    case "KITT":
                        return new Car();
                    }
                }
                return NOT_HANDLED;
            }

            @Override
            public JavaType handleMissingTypeId(final DeserializationContext ctxt, final JavaType baseType, final TypeIdResolver idResolver, final String failureMsg) throws IOException {
                // if (baseType.isTypeOrSubTypeOf(Vehicle.class)) {
                final JsonParser parser = ctxt.getParser();
                final String text = parser.getText();
                switch (text) {
                case "KITT":
                    return TypeFactory.defaultInstance().constructType(Car.class);
                }
                return super.handleMissingTypeId(ctxt, baseType, idResolver, failureMsg);
            }
        });

        final Container objectValue = om.readValue(getObjectJson(), Container.class);

        assertTrue(objectValue.getModeOfTransport() instanceof Car);

        final Container stringValue = om.readValue(getStringJson(), Container.class);

        assertTrue(stringValue.getModeOfTransport() instanceof Car);
    }

    private String getObjectJson() {
        return "{ \"modeOfTransport\": { \"type\": \"car\", \"name\": \"KITT\", \"speed\": 1}}";
    }

    private String getStringJson() {
        return "{ \"modeOfTransport\": \"KITT\"}";
    }
}

class Container {

    private Vehicle modeOfTransport;

    public Vehicle getModeOfTransport() {
        return modeOfTransport;
    }

    public void setModeOfTransport(final Vehicle modeOfTransport) {
        this.modeOfTransport = modeOfTransport;
    }
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true)
@JsonSubTypes({
        @Type(name = "car", value = Car.class)
})
abstract class Vehicle {

    protected String type;
    protected String name;

    public String getType() {
        return type;
    }

    public void setType(final String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public void setName(final String name) {
        this.name = name;
    }
}

@JsonTypeName("car")
class Car extends Vehicle {

    private int speed;

    public int getSpeed() {
        return speed;
    }

    public void setSpeed(final int speed) {
        this.speed = speed;
    }
}

Обратите внимание, что я использовал JSON, а не YAML, и вам нужно добавить и другие ваши подтипы.

...