5
@Entity
public Product {
   @Id
   public int id;

   public String name;

   @ManyToOne(cascade = {CascadeType.DETACH} )
   Category category

   @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.DETACH} )
   Set<Category> secondaryCategories;


}

and this entity:

@Entity
public Category {
   @Id
   public int id;

   public String name;
}

I would like to be able to send a POST with json

{ name: "name", category: 2, secondaryCategories: [3,4,5] } from client-side

and be able to be deserialized like:

{ name: "name", category: {id: 2 }, secondaryCategories: [{id: 3}, {id: 4}, {id: 5}] }

in case it was sent as

 { name: "name", category: {id: 2 }, secondaryCategories: [{id: 3}, {id: 4}, {id: 5}] }

I would like it to still work as now

what kind of annotation and custom deserializer I need? Hopefully the deserializer can work for all possible objects that have id as a property

Thanks!

Edit

Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
  • So you want serialization to `{ ... category : 1 ... }` and desalinize from `{ ... category : { id : 1 } ... }`(same behavior for secondaryCategories) Is that all you need or you actually want your code to both serialize and desalinize for both cases? – varren Oct 06 '17 at 14:38
  • @varren I care only for the part of deserialization.. check my coming edit to see what I tried – Michail Michailidis Oct 06 '17 at 16:01
  • @varren unfortunately it is not easy without using a ton of behavioral code to make that deserializer be a pass-through in case of a whole object according to: https://stackoverflow.com/a/18405958/986160 – Michail Michailidis Oct 06 '17 at 16:16
  • yeah passing default deserializer is a pain. Actually in your case you can just instantiate/autowire new (2-nd been)`ObjectMapper` inside `IdWrapperDeserializer` and it can work like default deserializer. (just don't register IdWrapperDeserializer in it) – varren Oct 06 '17 at 16:39

3 Answers3

2

There are several options you could try, actually custom deserializer/serializer would probably make sense, but you also can achieve this with @JsonIdentityInfo(for deserialization) + @JsonIdentityReference(if you need serialization as integer) annotations.


Deserialization

Work both for 
{ "category":1 }
{ "category":{ "id":1 }

So you need to annotate every class that can be deserialized from its id with @JsonIdentityInfo

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id", 
        scope = Product.class,  // different for each class
        resolver = MyObjectIdResolver.class)

The hard part here is that you actually have to write custom ObjectIdResolver that can resolve objects from your db/other source. Take a look at my simple reflection version in MyObjectIdResolver.resolveId method in example below:

private static class MyObjectIdResolver implements ObjectIdResolver {
    private Map<ObjectIdGenerator.IdKey,Object> _items  = new HashMap<>();

    @Override
    public void bindItem(ObjectIdGenerator.IdKey id, Object pojo) {
        if (!_items.containsKey(id)) _items.put(id, pojo);
    }

    @Override
    public Object resolveId(ObjectIdGenerator.IdKey id) {
        Object object = _items.get(id);
        return object == null ? getById(id) : object;
    }

    protected Object getById(ObjectIdGenerator.IdKey id){
        Object object = null;
        try {
            // todo objectRepository.getById(idKey.key, idKey.scope)
            object = id.scope.getConstructor().newInstance(); // create instance
            id.scope.getField("id").set(object, id.key);  // set id
            bindItem(id, object);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return object;
    }

    @Override
    public ObjectIdResolver newForDeserialization(Object context) {
        return this;
    }

    @Override
    public boolean canUseFor(ObjectIdResolver resolverType) {
        return resolverType.getClass() == getClass();
    }
}

Serialization

Default behavior
{ "category":{ "id":1 , "name":null} , secondaryCategories: [1 , { { "id":2 , "name":null} ]}

Default behavior is described here: https://github.com/FasterXML/jackson-databind/issues/372 and will produce object for the first element and id for each element after. An ID/reference mechanism in Jackson works so that an object instance is only completely serialized once and referenced by its ID elsewhere.

Option 1. (Always as id)

Works for 
{ "category":1 , secondaryCategories:[1 , 2]}

Need to use @JsonIdentityReference(alwaysAsId = true) above each object field(can uncomment in demo at the bottom of the page)

Option 2. (Always as full object representation)

Works for 
{ "category" : { "id":1 , "name":null} , secondaryCategories: [{ "id":1 , "name":null} , { "id":2 , "name":null}]}

This option is tricky because you will have to remove all the IdentityInfo for serialization somehow. One option could be to have 2 object mappers. 1 for serialization and 2-nd for deserialization and configure some sort of mixin or @JsonView

Another approach that is easier to implement is to use SerializationConfig to ignore @JsonIdentityInfo annotations completely

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();

    SerializationConfig config = mapper.getSerializationConfig()
            .with(new JacksonAnnotationIntrospector() {
        @Override
        public ObjectIdInfo findObjectIdInfo(final Annotated ann) {
            return null;
        }
    });

    mapper.setConfig(config);

    return mapper;
}

Probably the better approach would be to actually define @JsonIdentityInfo for deserializerconfig the same way and remove all annotations above classes. Something like this

At this point you probably wish you just wrote custom serializer/deserializer


Here is working (simple Jackson without spring) demo:

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;

public class Main {

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
            property = "id",
            resolver = MyObjectIdResolver.class,
            scope = Category.class)
    public static class Category {
        @JsonProperty("id")
        public int id;
        @JsonProperty("name")
        public String name;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

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

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
            property = "id",
            resolver = MyObjectIdResolver.class,
            scope = Product.class)
    public static class Product {
        @JsonProperty("id")
        public int id;
        @JsonProperty("name")
        public String name;

        // Need @JsonIdentityReference only if you want the serialization
        // @JsonIdentityReference(alwaysAsId = true)
        @JsonProperty("category")
        Category category;

        // Need @JsonIdentityReference only if you want the serialization
        // @JsonIdentityReference(alwaysAsId = true)
        @JsonProperty("secondaryCategories")
        Set<Category> secondaryCategories;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

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

    private static class MyObjectIdResolver implements ObjectIdResolver {

       private Map<ObjectIdGenerator.IdKey,Object> _items;

        @Override
        public void bindItem(ObjectIdGenerator.IdKey id, Object pojo) {
            if (_items == null) {
                _items = new HashMap<ObjectIdGenerator.IdKey,Object>();
            } if (!_items.containsKey(id))
                _items.put(id, pojo);
        }

        @Override
        public Object resolveId(ObjectIdGenerator.IdKey id) {
            Object object = (_items == null) ? null : _items.get(id);
            if (object == null) {
                try {

                    // create instance
                    Constructor<?> ctor = id.scope.getConstructor();
                    object = ctor.newInstance();

                    // set id
                    Method setId = id.scope.getDeclaredMethod("setId", int.class);
                    setId.invoke(object, id.key);
                    // https://github.com/FasterXML/jackson-databind/issues/372
                    // bindItem(id, object); results in strange behavior

                } catch (NoSuchMethodException | IllegalAccessException
                        | InstantiationException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
            return object;
        }

        @Override
        public ObjectIdResolver newForDeserialization(Object context) {
            return new MyObjectIdResolver();
        }

        @Override
        public boolean canUseFor(ObjectIdResolver resolverType) {
            return resolverType.getClass() == getClass();
        }
    }

    public static void main(String[] args) throws Exception {
        String str = "{ \"name\": \"name\", \"category\": {\"id\": 2 }, " +
                "\"secondaryCategories\":[{\"id\":3},{\"id\":4},{\"id\":5}]}";

        // from  str
        Product product = new ObjectMapper().readValue(str, Product.class);
        System.out.println(product);

        // to json
        String productStr = new ObjectMapper().writeValueAsString(product);
        System.out.println(productStr);

        String str2 = "{ \"name\": \"name\", \"category\":  2, " +
                "\"secondaryCategories\": [ 3,  4,  5] }";

        // from  str2
        Product product2 = new ObjectMapper().readValue(str2, Product.class);
        System.out.println(product2);

        // to json
        String productStr2 = new ObjectMapper().writeValueAsString(product2);
        System.out.println(productStr2);
    }
}
varren
  • 14,551
  • 2
  • 41
  • 72
  • seems it is working! I will accept when I make sure it does :) Thanks!! – Michail Michailidis Oct 06 '17 at 17:00
  • @MichailMichailidis yeah, check it, i'm not 100% sure in this jackson annotations magic; p Anyway, if this has its edge cases, you probably will have to go the custom deserializer way – varren Oct 06 '17 at 17:03
  • so while deserialization works I am getting a weird behavior in serialization even though I haven't explicitly configured anything! in a list of Products the first one has category embedded (as expected) and the rest has category as an int (its id).. uncommenting the annotations makes all categories embedded – Michail Michailidis Oct 06 '17 at 19:44
  • @MichailMichailidis probably related https://github.com/FasterXML/jackson-databind/issues/372 I actually noticed that this issue related to `ObjectIdResolver`implementation. take a look at new code and play with `bindItem(id, object);` in `resolveId`(i commented it near catch) If you create new object every time and return different objects even when their id's equal the code works as expected, but when you return same object multiple times spring just provides its ids and not the object itself. Probably some object mapper feature can disable it, but after the github link i'm not so sure) – varren Oct 06 '17 at 20:54
  • hm.. probably related to: https://github.com/FasterXML/jackson-databind/issues/1549 I had the same behavior when I am doing a GET with the new code.. curious how I could go about using ObjectMapper when I have already tried to read int and failed... I think I cannot reset it or get the string value of the object – Michail Michailidis Oct 06 '17 at 21:17
  • Another useful annotation you could check is @JsonIdentityInfo: using it, everytime jackson serializes your object, it will add an ID (or another attribute of your choose) to it, so that it won't entirely "scan" it again everytime. This can be useful when you've got a chain loop between more interrelated objects (for example: Order -> OrderLine -> User -> Order and over again ). from http://keenformatics.blogspot.gr/2013/08/how-to-solve-json-infinite-recursion.html – Michail Michailidis Oct 06 '17 at 21:24
  • @MichailMichailidis updated post with some serialization thoughts. Need to test it though. Don't have much time right now) – varren Oct 07 '17 at 08:09
  • Thanks for the effort - in the meantime I used your suggestion for a new objectMapper and I was able to make it work without too much configuration – Michail Michailidis Oct 07 '17 at 09:44
2

Another approach is to use @JsonCreator factory method if you can modify your Entity

private class Product {
    @JsonProperty("category")
    private Category category;

    @JsonProperty("secondaryCategories")
    private List<Category> secondaryCategories;
}


private class Category {
    @JsonProperty("id")
    private int id;

    @JsonCreator
    public static Category factory(int id){
        Category p = new Category();
        p.id = id;
        // or some db call 
        return p;
    }
}

Or even something like this should also work

private class Category {
    private int id;

    public Category() {}

    @JsonCreator
    public Category(int id) {
        this.id = id;
    }
}
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
zafrost
  • 576
  • 3
  • 3
  • if `@JsonCreator` works during deserialization this will work for embedded categories sending just the ids but not when I am sending the whole object.. e.g during a POST/PUT of a single Category or for backwards compatibility with embedded categories – Michail Michailidis Oct 07 '17 at 09:41
  • @MichailMichailidis hm, it works pretty fine for me. Jackson can have multiple @JsonCreator for each type you want. And it will still use default property-based creator if you json looks like object. It will work for both `[1, 2]` and `[{"id":1},{"id":2}]` and even `[{"id":2},1]` the only problem with this approach is that you actually have to use factory methods in entity beens and not in some config class – zafrost Oct 07 '17 at 10:10
  • are you sure about the part of multiple `@JsonCreator`s per type ? https://stackoverflow.com/a/15931483/986160 – Michail Michailidis Oct 07 '17 at 10:14
  • @MichailMichailidis yes 100% sure. they are talking about `@JsonCreator` with the same argument type, like `Category factory(@JsonProperty("id") int id)` and `Category factory(@JsonProperty("someotherid") int id)` Here is the order of json creators: https://github.com/FasterXML/jackson-databind/blob/2.7/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCollector.java#L23-L31 But you can have multiple creators with different types or just use different constructors (updated post a littel bit) – zafrost Oct 07 '17 at 10:17
  • I will give it a try and tell you in a bit :) – Michail Michailidis Oct 07 '17 at 10:35
  • wow it worked like a charm @zafrost! I wish you had responded yesterday so all those hours weren't wasted - I chose your second approach - even `JsonProperty("id")` is not needed - I added a `@JsonCreator` over your constructor too - editted your answer – Michail Michailidis Oct 07 '17 at 11:23
  • any chance you know the inverse of this problem too: https://stackoverflow.com/questions/46620019/single-custom-serializer-for-all-annotated-embedded-objects-that-replaces-them-w – Michail Michailidis Oct 07 '17 at 12:23
0

The complete solution after much struggle was - thanks to https://stackoverflow.com/users/1032167/varren's comment and https://stackoverflow.com/a/16825934/986160 I was able to use the default deserialization (through a local new objectMapper) in my StdDeserializer without the hurdles in this answer: https://stackoverflow.com/a/18405958/986160

The code tries to parse an int and if it is a whole object it just passes it through - so it still works for example when you make a POST/PUT request of a Category or in other words when Category is not embedded

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.context.annotation.Bean;

import java.io.IOException;

public class IdWrapperDeserializer<T> extends StdDeserializer<T> {

    private Class<T> clazz;

    public IdWrapperDeserializer(Class<T> clazz) {
        super(clazz);
        this.clazz = clazz;
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        return mapper;
    }

    @Override
    public T deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
        String json = jp.readValueAsTree().toString();

        T obj = null;
        int id = 0;
        try {
            id = Integer.parseInt(json);
        }
        catch( Exception e) {
            obj = objectMapper().readValue(json, clazz);
            return obj;
        }
        try {
            obj = clazz.newInstance();
            ReflectionUtils.set(obj,"id",id);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return obj;
    }

}

for each entity I need to behave like described I need to configure it in global ObjectMapper Bean of the Spring Boot application:

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
    mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
    mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

    SimpleModule testModule = new SimpleModule("MyModule")
            .addDeserializer(Category.class, new IdWrapperDeserializer(Category.class))

    mapper.registerModule(testModule);

    return mapper;
}

This is my ReflectionUtils from https://stackoverflow.com/a/14374995/986160

public class ReflectionUtils {
    // 
    public static boolean set(Object object, String fieldName, Object fieldValue) {
        Class<?> clazz = object.getClass();
        while (clazz != null) {
            try {
                Field field = clazz.getDeclaredField(fieldName);
                field.setAccessible(true);
                field.set(object, fieldValue);
                return true;
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }
        return false;
    }
}
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106