简体   繁体   中英

How to do a general custom deserializer for a parameterized interface type in Jackson

I have an interface with numerous implementation classes and I would like to write a general deserializer instead of a deserializer for each implementation:

The interface:

public interface IEnumerable<E extends Enum<E>> {
    public String getName();
}

Abstract class that uses reflection to do a reverse lookup on the Enumerable:

public abstract class AbstractEnumerable<E extends Enum<E> & IEnumerable<E>> {
    private static final XLogger log = XLoggerFactory.getXLogger(AbstractEnumerable.class.getCanonicalName());

    private final TypeToken<AbstractEnumerable<E>> typeToken = new TypeToken<AbstractEnumerable<E>>(getClass()) { };

    public final E getByName(String name) {
        TypeToken<?> genericParam = typeToken.resolveType(AbstractEnumerable.class.getTypeParameters()[0]);
        log.debug("Runtime class of generic IEnumerable parameter: {}", genericParam.getType().getTypeName());

        try {
            log.trace("Getting a Class object for {}", genericParam.getType().getTypeName());
            Class<E> clazz = (Class<E>)Class.forName(genericParam.getType().getTypeName());

            log.trace("Iterating over the enum constants in {}", genericParam.getType().getTypeName());
            for(Object o : Arrays.asList(clazz.getEnumConstants())) {
                E val = clazz.cast(o);
                if(val.getName().equals(name) || val.name().equals(name)) {
                    return val;
                }
            }
        } catch(ClassNotFoundException e) {
            log.error("Unable to find the class definition for {}", genericParam.getType().getTypeName());
            log.catching(e);
        }

        return null;
    }
}

Implementation:

public class DnsRecordTypeEnumeration extends AbstractEnumerable<DnsRecordTypeEnumeration.DnsRecordType> {
    public static enum DnsRecordType implements IEnumerable<DnsRecordType> {
        DNS_TYPE_A("A"),
        DNS_TYPE_AAAA("AAAA"),
        DNS_TYPE_CNAME("CNAME");

        private final String localizedName;

        private DnsRecordType(final String localizedName) {
            this.localizedName = localizedName;
        }

        @Override
        public final String getName() {
            return localizedName;
        }
    }
}

Is it possible to have a general custom deserializer for all Enumerable implementations? I need to access the enclosing class to do the reverse lookup.

I tried this:

public abstract class AbstractJacksonJsonDeserializer<T> extends JsonDeserializer<T> {
        private static final XLogger log = XLoggerFactory.getXLogger(AbstractJacksonJsonDeserializer.class
                .getCanonicalName());

        private final TypeToken<AbstractJacksonJsonDeserializer<T>> typeToken =
                new TypeToken<AbstractJacksonJsonDeserializer<T>>(getClass()) { };

        protected Class<T> getTypeClass() {
            Class<T> clazz = (Class<T>)RuntimeClassFactory.getInstance().create(typeToken, AbstractJacksonJsonDeserializer.class, 0);
            log.debug("Runtime class of object to be deserialized: {}", clazz.getCanonicalName());
            return (Class<T>)RuntimeClassFactory.getInstance().create(typeToken, AbstractJacksonJsonDeserializer.class, 0);
        }

public class EnumerableJacksonJsonDeserializer<E extends Enum<E> & IEnumerable<E>> extends AbstractJacksonJsonDeserializer<E> {

    @Override
    public E deserialize(final JsonParser parser, final DeserializationContext
            context) throws JsonProcessingException, IOException {
        JsonNode node = parser.getCodec().readTree(parser);
        String name = node.textValue();
        return getEnumeration().getByName(name);
    }

    protected <T extends AbstractEnumerable<E>> T getEnumeration() {
        Class<E> enumerableClass = getTypeClass();
        Class<T> enumerationClass = (Class<T>) enumerableClass.getEnclosingClass();
        return EnumerationFactory.getInstance().create(enumerationClass);
    }
}

Problem is that I can't annotate fields with the above class like this because my custom deserializer takes a type parameter. This results in an compile error:

@JsonDeserialize(using = EnumerableJacksonJsonDeserializer.class)
private DnsRecordType recordType;

You can write a module that provides deserializers based on the context they are encountered in: this allows you to get the concrete type when a subclass of IEnumerable is encountered.

I'm assuming some of the external bits you refer to do actually exist, like EnumerationFactory. Tbh the whole thing with "enumeration" wrapping an enum confused me, but this seems to follow what you put down:

@Test
public void calls_general_deserializer_for_parameterized_interface() throws Exception {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new SimpleModule() {
        @Override
        public void setupModule(SetupContext context) {
            context.addDeserializers(new Deserializers.Base() {
                @Override
                public JsonDeserializer<?> findEnumDeserializer(Class<?> type,
                        DeserializationConfig config,
                        BeanDescription beanDesc) throws JsonMappingException {
                    if (IEnumerable.class.isAssignableFrom(type)) return new EnumerableDeserializer(type);
                    return null;
                }
            });
            super.setupModule(context);
        }
    });
    TestData data = mapper.readerFor(TestData.class).with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
            .with(JsonParser.Feature.ALLOW_SINGLE_QUOTES).readValue("{ recordType: 'AAAA' }");
    assertThat(data.recordType, equalTo(DnsRecordType.DNS_TYPE_AAAA));
}

public static final class EnumerableDeserializer<E extends Enum<E> & IEnumerable<E>> extends
        StdScalarDeserializer<E> {
    private final Class<? extends AbstractEnumerable<E>> enumerationClass;
    private final Class<E> enumerableClass;

    public EnumerableDeserializer(Class<E> enumerableClass) {
        super(enumerableClass);
        this.enumerableClass = enumerableClass;
        this.enumerationClass = (Class<? extends AbstractEnumerable<E>>) enumerableClass.getEnclosingClass();
    }

    @Override
    public E deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        AbstractEnumerable<E> enumerable = getEnumeration();
        return enumerable.getByName(p.getText());
    }

    private AbstractEnumerable<E> getEnumeration() {
        return EnumerationFactory.getInstance().create(enumerationClass);
    }
}

Jackson's existing Enum deserialization doesn't seem to support allowing each enum instance to have two possible string values, which seems to be what you need here. TBH it seems you could dump the enclosing enumeration class and just write the deserializer more simply:

public static final class DirectEnumerableDeserializer<E extends Enum<E> & IEnumerable<E>> extends
        StdScalarDeserializer<E> {
    private final Class<E> enumerableClass;
    private final ImmutableList<E> values;
    private final ImmutableMap<String, E> names;

    public DirectEnumerableDeserializer(Class<E> enumerableClass) {
        super(enumerableClass);
        this.enumerableClass = enumerableClass;
        this.values = ImmutableList.copyOf(enumerableClass.getEnumConstants());
        ImmutableMap.Builder<String, E> names = ImmutableMap.builder();
        for (E value : values) {
            names.put(value.name(), value);
        }
        this.names = names.build();
    }

    @Override
    public E deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        String key = p.getText();
        E value = names.get(key);
        if (value == null) {
            value = findByLocalisedName(key);
            if (value == null) throw ctxt.weirdStringException(key, enumerableClass, "Unrecognised name");
        }
        return value;
    }

    private E findByLocalisedName(String key) {
        for (E value : values) {
            if (value.getName().equals(key)) return value;
        }
        return null;
    }
}

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