2

I have a class hierarchy that looks as follows:

interface MyEntity {
    ...
}

class MyEntityRef {
    String id;
    ...
}

class MyEntityImpl {
    String id;
    String name;
    ...
}

I would like to serialize instances of MyEntityRef as a plain string:

"someEntityId"

and instances of MyEntityImpl as a regular object:

{
    id: "someEntityId",
    name: "someName"
}

And then the other way around: When deserializing a MyEntity I'd like it to be deserialized into a MyEntityRef if the json is a plain string, and into a MyEntityImpl if it's a regular json object.

In my actual code I have many types of entities (i.e. multiple X/XRef/XImpl triples). In an attempt to avoid listing all of these, I've annotated the interfaces as follows:

@MyEntityAnnotation(ref=MyEntityRef.class, impl=MyEntityImpl.class)
interface MyEntity {
    ...
}

Extra points to whomever figures out a way to solve the serialization/deserialization described above for all entities based this annotation information.


What I've tried: Everything I can think of (SimpleDeserializers/SimpleSerializers, BeanDeserializerModifier/BeanSerializerModifier, TypeSerializer/TypeDeserializer)

aioobe
  • 413,195
  • 112
  • 811
  • 826
  • By "plain string", [since that's not valid JSON](http://stackoverflow.com/questions/24184296/is-a-plain-string-valid-json), are you just speaking about producing the value in java code? – Sam Berry Jun 12 '14 at 13:05
  • I should have written *"json value"* instead. (A plain string should be a valid *value* just as `17` is a valid JSON value, right?) – aioobe Jun 12 '14 at 13:08
  • Ok yep, that makes sense--just clarifying. – Sam Berry Jun 12 '14 at 13:26

1 Answers1

2

This is technically possible by using a custom deserializer (see below) but seems to be quite complicated and a bit clunky, with the use of reflection and type unsafe conversions. You may get better luck using the bi-directional references, but that may require changing your model.

Here is an example based on a custom deserializer:

public class JacksonEntity {

    @Retention(RetentionPolicy.RUNTIME)
    static public @interface MyAnnotation {
        Class<?> ref();
        Class<?> impl();
    }

    @MyAnnotation(ref = MyEntityRef.class, impl = MyEntityImpl.class)
    static public interface MyEntity {
    }

    static public class MyEntityRef implements MyEntity {
        private final String id;

        public MyEntityRef(String id) {
            this.id = id;
        }

        @JsonValue
        public String getId() {
            return id;
        }

        @Override
        public String toString() {
            return "MyEntityRef{" +
                    "id='" + id + '\'' +
                    '}';
        }
    }

    static public class MyEntityImpl implements MyEntity {
        public final String id;
        public final String name;

        @JsonCreator
        public MyEntityImpl(@JsonProperty("id") String id, @JsonProperty("name") String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return "MyEntityImpl{" +
                    "id='" + id + '\'' +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    static public class MyDeserializer extends JsonDeserializer<Object> {
        private final Class<?> refType;
        private final Class<?> implType;

        public MyDeserializer(MyAnnotation annotation) {
            this.refType = annotation.ref();
            this.implType = annotation.impl();
        }

        @Override
        public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
            JsonToken jsonToken = jp.getCurrentToken();
            if (jsonToken == JsonToken.START_OBJECT) {
                return jp.readValueAs(implType);
            } else if (jsonToken == JsonToken.VALUE_STRING) {
                try {
                    return refType.getConstructor(String.class).newInstance(jp.getValueAsString());
                } catch (Exception e) {
                    throw new UnsupportedOperationException();
                }
            }
            return null;
        }
    }

    public static void main(String[] args) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.setDeserializerModifier(new BeanDeserializerModifier() {
            @Override
            public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
                                                          BeanDescription beanDesc,
                                                          JsonDeserializer<?> deserializer) {
                MyAnnotation myAnnotation = beanDesc.getClassAnnotations().get(MyAnnotation.class);
                // it must be interface, otherwise getting meeting recursion
                if (myAnnotation != null && beanDesc.getBeanClass().isInterface()) {
                    return new MyDeserializer(myAnnotation);
                }
                return super.modifyDeserializer(config, beanDesc, deserializer);
            }
        });
        mapper.registerModule(module);

        MyEntityRef ref = new MyEntityRef("id1");
        MyEntityImpl impl = new MyEntityImpl("id2", "value");

        String jsonRef = mapper.writeValueAsString(ref);
        System.out.println(jsonRef);
        String jsonImpl = mapper.writeValueAsString(impl);
        System.out.println(jsonImpl);

        System.out.println(mapper.readValue(jsonRef, MyEntity.class));
        System.out.println(mapper.readValue(jsonImpl, MyEntity.class));
    }
}

Output:

"id1"
{"id":"id2","name":"value"}
MyEntityRef{id='id1'}
MyEntityImpl{id='id2', name='value'}
Alexey Gavrilov
  • 10,593
  • 2
  • 38
  • 48