Search code examples
javajacksonpolymorphismdeserializationopenapi

How to prevent Jackson InvalidTypeIdExceptions at client side?


Let's say we have a base class Pet, with subclasses Dog, Cat and Fish defined as our service response:

openapi: '3.0.3'
paths:
  '/pets':
    get:
      responses:
        200:
          description: Ok
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Pet'
components:
  schemas:
    Pet:
      type: object
      description: Base class for all types of pets
      properties:
        kind:
          type: string
      required:
        - kind
      discriminator:
        propertyName: kind
        mapping:
          Dog: '#/components/schemas/Dog'
          Cat: '#/components/schemas/Cat'
          Fish: '#/components/schemas/Fish'

We have several clients consuming our service using this OpenApi definition.

Now, we want to add a fourth pet type, Snakes, so we add it to the "mapping" section of the Pet schema. But if we deploy such a change to our service production system, our clients will fail to parse the service responses whenever a Snake is returned.

We can publish our new improved API in advance and give some time to our clients for upgrading and add support for this new type, but we cannot afford to delay the upgrade indefinitely. Therefore, we want to be able to deploy our new service version and send snakes in its response without breaking things at our clients side.

Is there any way to define this polymorphic type in an "open" way, so clients can parse snakes and other future unknown-yet types without getting an InvalidTypeIdException? I don't need to be able to get the snake fields (I am aware of the security issues involved), I would be happy to have just a "Pet" instance with kind set to "Snake", and no other info.

If this cannot be accomplished at the server side, is there any way to get client side code generated to be resilient against these errors?

If no solution exists at either definition or client code generation, could such a feature be implemented (at the client side) in Java using Jackson annotations or configuration?


Solution

  • So far, I have not found any way to get what we are looking for from the server side, or through the OpenApi spec. However, I have found two possible ways to get it from the client side using Jackson.

    The first way is by disabling FAIL_ON_INVALID_SUBTYPE at the ObjectMapper:

    var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE);
    

    Which will make the unknown subtype instances to be deserialized into nulls. It is important to take this into account for Pet lists, as they may have null items in the future.

    The other way would be to create a DeserializationProblemHandler subclass that would force the unknown instance to be deserialized into an instance of the root type:

    public class UnknownPetTypeHandler extends DeserializationProblemHandler {
      @Override
      public JavaType handleUnknownTypeId(DeserializationContext ctx,
          JavaType baseType, String subTypeId,
          TypeIdResolver idResolver, String failureMsg) {
        return Pet.class.equals(baseType.getRawClass()) ? baseType : null;
      }
    }
    

    and add it to the ObjectMapper used for deserialization:

    var mapper = new ObjectMapper().addHandler(new UnknownPetTypeHandler());
    

    This second option avoids null values and provides a mechanism for detecting and notifying on unknown types received, at the cost of extra complexity.