简体   繁体   中英

Jackson Custom Deserializer for polymorphic objects and String literals as defaults

I'd like to deserialize an object from YAML with the following properties, using Jackson in a Spring Boot application:

  • Abstract class Vehicle , implemented by Boat and Car
  • For simplicity, imagine both have a name, but only Boat has also a seaworthy property, while Car has a top-speed .
mode-of-transport:
  type: boat
  name: 'SS Boatface'
  seaworthy: true
----
mode-of-transport:
  type: car`
  name: 'KITT'
  top-speed: 123

This all works fine in my annotated subclasses using @JsonTypeInfo and @JsonSubTypes !

Now, I'd like to create a shorthand using only a String value, which should create a Car by default with that name:

mode-of-transport: 'KITT'

I tried creating my own custom serializer, but got stuck on most of the relevant details. Please help me fill this in, if this is the right approach:

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? */
   }
}

I found these 2 answers for inspiration, but it looks like combining it with polymorphic types makes it more difficult: How do I call the default deserializer from a custom deserializer in Jackson and Deserialize to String or Object using Jackson

A few things are different than the solutions offered in those questions:

  • I am processing YAML, not JSON. Not sure about the subtle differences there.
  • I have no problem hardcoding the 'default' type for Strings inside my Deserializer, hopefully making it simpler.

This was actually easier than I thought to solve it. I got it working using the following:

  1. Custom deserializer implementation:
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);
    }
}
  1. To avoid circular dependencies and to make the custom deserializer work with the polymorphic @JsonTypeInfo and @JsonSubTypes annotations I kept those annotations on the class level of Vehicle , but put the following annotations on the container object I am deserializing:
public class Transport {

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

    // Getter, setters
}

This means that by default a Vehicle is deserialized as a polymorphic object, unless explicitly specified to deserialize it using my custom deserializer. This deserializer will then in turn defer to the polymorphism if the input is not a String.

Hopefully this will help someone running into this issue :)

So there is a solution that requires you to handle the jackson errors using a DeserializationProblemHandler (since you want to parse the same type using different inputs, this is not achieved easily using regular means):

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

Note that I used JSON, not YAML, and you need to add your other subtypes as well.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM