oberlies
oberlies

Reputation: 11723

Jackson: How to make @JsonBackReference and a custom deserializer work at the same time?

I have a JSON structure which contains maps of strings to object:

{
    "domains": {
        "ldap": {
            "users": {
                "fwalther": {
                    "firstName": "Fritz",
                    "lastName": "Walther"
                },
                // ...
            }
        }
    },
    // ...
}       

I want to deserialize this structure to Domain and User objects using Jackson, and I want from each user have a back-reference to it's map key (which is the user ID) and to the Domain container. Either of this works, but it fails if I try to get both back-references at once.

Java classes with @JsonManagedReference and @JsonBackReference:

public class Domain {
    @JsonManagedReference
    @JsonDeserialize(contentUsing = UserDeserializer.class)
    private Map<String, User> users;

    public Map<String, User> getUsers() {
        return users;
    }
}

public class User {
    @JsonBackReference
    private Domain domain;

    String userId;

    private String firstName;
    private String lastName;

    // ... getters
}

Custom deserializer to get the map key:

public class UserDeserializer extends JsonDeserializer<User> {
    @Override
    public User deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        String key = p.getCurrentName();
        User result = p.readValueAs(User.class);

        result.userId = key;
        return result;
    }
}

Both mechanisms, i.e. the @JsonManagedReference/@JsonBackReference pair and @JsonDeserialize with the custom deserializer, work if I activate only one of them. But if I combine the mechanisms (as shown in the code above), I get the following exception when parsing the JSON:

java.lang.IllegalArgumentException: Cannot handle managed/back reference 'defaultReference': type: value deserializer of type org.example.UserDeserializer does not support them
    at com.fasterxml.jackson.databind.JsonDeserializer.findBackReference(JsonDeserializer.java:366) ~[jackson-databind-2.9.8.jar:2.9.8]
    at com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase.findBackReference(ContainerDeserializerBase.java:104) ~[jackson-databind-2.9.8.jar:2.9.8]
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase._resolveManagedReferenceProperty(BeanDeserializerBase.java:786) ~[jackson-databind-2.9.8.jar:2.9.8]
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.resolve(BeanDeserializerBase.java:487) ~[jackson-databind-2.9.8.jar:2.9.8]
...

Looking at the code where the exception is thrown, I see that I'd need to implement the findBackReference in my custom deserializer, but I have no clue how, and I couldn't find and information on this either. Any ideas?

Or are there other ways to get both the map key and back reference to the containing object?

Upvotes: 2

Views: 1501

Answers (1)

oberlies
oberlies

Reputation: 11723

With the help of this answer, I found the solution: The custom deserializer needs to be based on the default deserializer, which implements the back references mechanism correctly.

This is a little more complicated than just inheriting from the right base class. Instead, you need to get hold of the (fully configured) default deserializer instance through a custom BeanDeserializerModifier, and then pass this instance to your subclass of DelegatingDeserializer:

public ObjectMapper getMapperWithCustomDeserializer() {
    ObjectMapper objectMapper = new ObjectMapper();

    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier() {
        @Override
        public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
                    BeanDescription beanDesc, JsonDeserializer<?> defaultDeserializer) {
            if (beanDesc.getBeanClass() == User.class) {
                return new UserDeserializer(defaultDeserializer);
            } else {
                return defaultDeserializer;
            }
        }
    });
    objectMapper.registerModule(module);

    return objectMapper;
}

The custom deserializer then would need to look like this:

public class UserDeserializer extends DelegatingDeserializer {

    public UserDeserializer(JsonDeserializer<?> delegate) {
        super(delegate);
    }

    @Override
    protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegate) {
        return new UserDeserializer(newDelegate);
    }

    @Override
    public User deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException {
        String key = p.getCurrentName();
        User result = (User) super.deserialize(p, ctxt);

        result.userId = key;
        return result;
    }
}

Finally, you need to remove the @JsonDeserialize annotation. Then, the custom deserializer and the @JsonBackReference should work.

Upvotes: 2

Related Questions