'Jackson deserializing adapter/converter with context

I have a simple set of classes that I want to serialize to/deserialize from YAML using Jackson (2.4.5):

public static class Animal {
    public String name;
}

public static class Dog extends Animal {
    public String breed;
}

public static class Cat extends Animal {
    public String favoriteToy;
}

public static class AnimalRegistry {
    private Map<String, Animal> fAnimals = new HashMap<>();

    public AnimalRegistry(Animal... animals) {
        for (Animal animal : animals)
            fAnimals.put(animal.name, animal);
    }

    public Animal getAnimal(String name) {
        return fAnimals.get(name);
    }
}

It is fairly straight forward to do that so that the AnimalRegistry ends up as a list of nested objects of Animal (subtype) objects. I can write and read those just fine. The problem I'm facing is to separately serialize/deserialize objects of another class:

public static class PetOwner {
    public String name;
    public List<Animal> pets = new ArrayList<>();
}

I don't want to serialize the Animal objects as a list of nested objects, but rather only store a list of the Animal's names. When deserializing I want to map those names back to Animal objects using a pre-existing AnimalRegistry.

With JAXB I could simply do that using an XmlAdapter:

public static class PetXmlAdapter extends XmlAdapter<String, Animal> {
    private AnimalRegistry fRegistry;

    public PetXmlAdapter(AnimalRegistry registry) {
        fRegistry = registry;
    }

    @Override
    public Animal unmarshal(String value) throws Exception {
        return fRegistry.getAnimal(value);
    }

    @Override
    public String marshal(Animal value) throws Exception {
        return value.name;
    }
}

I'd annotate the pets field with

    @XmlJavaTypeAdapter(value = PetXmlAdapter.class)

and add an instance of PetXmlAdapter to the Marshaller/Unmarshaller:

    marshaller.setAdapter(new PetXmlAdapter(animalRegistry));
    ...
    unmarshaller.setAdapter(new PetXmlAdapter(animalRegistry));

Jackson supports the JAXB annotations and could use the same PetXmlAdapter class, but I don't see a way to set an instance of it on the ObjectMapper or any related class and thus cannot use my pre-existing AnimalRegistry.

Jackson seems to have a lot of points for customization and in the end I found a way to achieve my goal:

public static class AnimalNameConverter
        extends StdConverter<Animal, String> {
    @Override
    public String convert(Animal value) {
        return value != null ? value.name : null;
    }
}

public static class NameAnimalConverter
        extends StdConverter<String, Animal> {
    private AnimalRegistry fRegistry;

    public NameAnimalConverter(AnimalRegistry registry) {
        fRegistry = registry;
    }

    @Override
    public Animal convert(String value) {
        return value != null ? fRegistry.getAnimal(value) : null;
    }
}

public static class AnimalSerializer
        extends StdDelegatingSerializer {
    public AnimalSerializer() {
        super(Animal.class, new AnimalNameConverter());
    }

    private AnimalSerializer(Converter<Object,?> converter,
            JavaType delegateType,
            JsonSerializer<?> delegateSerializer) {
        super(converter, delegateType, delegateSerializer);
    }

    @Override
    protected StdDelegatingSerializer withDelegate(
            Converter<Object, ?> converter, JavaType delegateType,
            JsonSerializer<?> delegateSerializer) {
        return new AnimalSerializer(converter, delegateType,
            delegateSerializer);
    }
}

public static class AnimalDeserializer
        extends StdDelegatingDeserializer<Animal> {
    private static final long serialVersionUID = 1L;

    public AnimalDeserializer(AnimalRegistry registry) {
        super(new NameAnimalConverter(registry));
    }

    private AnimalDeserializer(Converter<Object, Animal> converter,
            JavaType delegateType,
            JsonDeserializer<?> delegateDeserializer) {
        super(converter, delegateType, delegateDeserializer);
    }

    @Override
    protected StdDelegatingDeserializer<Animal> withDelegate(
            Converter<Object, Animal> converter,
            JavaType delegateType,
            JsonDeserializer<?> delegateDeserializer) {
        return new AnimalDeserializer(converter, delegateType,
            delegateDeserializer);
    }
}

public static class AnimalHandlerInstantiator
        extends HandlerInstantiator {
    private AnimalRegistry fRegistry;

    public AnimalHandlerInstantiator(AnimalRegistry registry) {
        fRegistry = registry;
    }

    @Override
    public JsonDeserializer<?> deserializerInstance(
            DeserializationConfig config, Annotated annotated,
            Class<?> deserClass) {
        if (deserClass != AnimalDeserializer.class)
            return null;
        return new AnimalDeserializer(fRegistry);
    }

    @Override
    public KeyDeserializer keyDeserializerInstance(
            DeserializationConfig config, Annotated annotated,
            Class<?> keyDeserClass) {
        return null;
    }

    @Override
    public JsonSerializer<?> serializerInstance(
            SerializationConfig config, Annotated annotated,
            Class<?> serClass) {
        return null;
    }

    @Override
    public TypeResolverBuilder<?> typeResolverBuilderInstance(
            MapperConfig<?> config, Annotated annotated,
            Class<?> builderClass) {
        return null;
    }

    @Override
    public TypeIdResolver typeIdResolverInstance(
            MapperConfig<?> config,
            Annotated annotated, Class<?> resolverClass) {
        return null;
    }
}

I annotate the pets field with

    @JsonSerialize(contentUsing = AnimalSerializer.class)
    @JsonDeserialize(contentUsing = AnimalDeserializer.class)

and set an instance of AnimalHandlerInstantiator on the ObjectMapper:

    mapper.setHandlerInstantiator(
        new AnimalHandlerInstantiator(animalRegistry));

This works, but it is an awful lot of code. Can anyone suggest a more concise alternative? I'd like to avoid writing a serializer/deserializer for PetOwner that requires manual handling of fields other than pets, though.



Solution 1:[1]

If I'm reading your question correctly, your serialized PetOwner records don't need to contain full Animal records -- they just need to contain a String for each Animal record. And your complications come from the fact that you are storing Animal records nonetheless.

Assuming that's an accurate statement, one approach would be to annotate your the 'pets' field (or the getter, not sure what your full PetOwner class looks like) with @JsonIgnore, then add a getter such as:

public List<String> getAnimalNames() {
  // return a list containing the name of each Animal in this.pets 
}

Then, you can either:

  1. Add an alternate constructor for PetOwner that takes a List<String> rather than a List<Animal>, and annotate that constructor with @JsonCreator so that Jackson knows to use it (and construct your Animal instances from the provided names inside the constructor)
  2. If you're using a default (0-arg) constructor, add a setter such as (and remove any existing setters for pets):

public void setAnimalNames(List<String> names) { // populate this.pets by looking up each pet by name in your registry }

I would have to see more of your PetOwner class and/or know more about how you plan to use it to give a more detailed response, but I think this general approach (annotate PetOwner class so that Jackson will just ignore 'pets' and deal only in 'animalNames' instead) should give you what you're looking for.

Solution 2:[2]

Here is a different approach for this problem without usign a converter, but handling the conversion directly in the deserializer:

        AnimalRegistry fRegistry = ...
        ObjectMapper objectMapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.setDeserializerModifier(new BeanDeserializerModifier() {
            @Override
            public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
                                                          JsonDeserializer<?> deserializer) {
                if (Animal.class.isAssignableFrom(beanDesc.getBeanClass())) {
                    return new AnimalDeserializer(fRegistry, deserializer, beanDesc.getBeanClass());
                }
                return deserializer;
            }
        });
        objectMapper.registerModule(module);
public class AnimalDeserializer extends StdDeserializer<Animal> implements ResolvableDeserializer {

    private static final long serialVersionUID = 1L;

    private final JsonDeserializer<?> defaultDeserializer;

private final AnimalRegistry registry;

    public AnimalDeserializer(AnimalRegistry, JsonDeserializer<?> defaultDeserializer, Class<?> clazz) {
        super(clazz);
        this.registry = registry;
        this.defaultDeserializer = defaultDeserializer;
    }

    @Override
    public Animal deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        Animal animal = (Animal) defaultDeserializer.deserialize(parser, context);
        // no add here your converter logic and filter the animals
        return Animal;
    }

    @Override
    public void resolve(DeserializationContext ctxt) throws JsonMappingException {
        ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
    }

}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 k_o_