3

I am consuming a "RESTful" service (via RestTemplate) that produces JSON as follows:

{
    "id": "abcd1234",
    "name": "test",
    "connections": {
        "default": "http://foo.com/api/",
        "dev": "http://dev.foo.com/api/v2"
    },
    "settings": {
        "foo": "{\n \"fooId\": 1, \"token\": \"abc\"}",
        "bar": "{\"barId\": 2, \"accountId\": \"d7cj3\"}"
    }
}

Note the settings.foo and settings.bar values, which cause issues on deserialization. I would like to be able to deserialize into objects (e.g., settings.getFoo().getFooId(), settings.getFoo().getToken()).

I was able to solve this specifically for an instance of Foo with a custom deserializer:

public class FooDeserializer extends JsonDeserializer<Foo> {
    @Override
    public Foo deserialize(JsonParser jp, DeserializationContext ctx) throws IOException {
        JsonNode node = jp.getCodec().readTree(jp);

        String text = node.toString();
        String trimmed = text.substring(1, text.length() - 1);
        trimmed = trimmed.replace("\\", "");
        trimmed = trimmed.replace("\n", "");

        ObjectMapper mapper = new ObjectMapper();
        JsonNode obj = mapper.readTree(trimmed);
        Foo result = mapper.convertValue(obj, Foo.class);

        return result;
    }
}

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Settings {
    @JsonDeserialize(using = FooDeserializer.class)
    private Foo foo;

    private Bar bar;
}

However, now if I want to deserialize settings.bar, I need to implement another custom deserializer. So I implemented a generic deserializer as follows:

public class QuotedObjectDeserializer<T> extends JsonDeserializer<T> implements ContextualDeserializer {
    private Class<?> targetType;
    private ObjectMapper mapper;

    public QuotedObjectDeserializer(Class<?> targetType, ObjectMapper mapper) {
        this.targetType = targetType;
        this.mapper = mapper;
    }

    @Override
    public JsonDeserializer<T> createContextual(DeserializationContext context, BeanProperty property) {
        this.targetType = property.getType().containedType(1).getRawClass();
        return new QuotedObjectDeserializer<T>(this.targetType, this.mapper);
    }

    @Override
    public T deserialize(JsonParser jp, DeserializationContext context) throws IOException {
        JsonNode node = jp.getCodec().readTree(jp);
        String text = node.toString();
        String trimmed = text.substring(1, text.length() - 1);
        trimmed = trimmed.replace("\\", "");
        trimmed = trimmed.replace("\n", "");

        JsonNode obj = this.mapper.readTree(trimmed);
        return this.mapper.convertValue(obj, this.mapper.getTypeFactory().constructType(this.targetType));
    }
}

Now I'm not sure how to actually use the deserializer, as annotating Settings.Foo with @JsonDeserialize(using = QuotedObjectDeserializer.class) obviously does not work.

Is there a way to annotate properties to use a generic custom deserializer? Or, perhaps more likely, is there a way to configure the default deserializers to handle the stringy objects returned in my example JSON?

Edit: The problem here is specifically deserializing settings.foo and settings.bar as objects. The JSON representation has these objects wrapped in quotes (and polluted with escape sequences), so they are deserialized as Strings.

LiquidPony
  • 2,188
  • 1
  • 17
  • 19
  • Did you try to create POJO object of Json data and Use jackson to retrieve it ? If you create first POJO and then call settings.getFoo().getFooId() It will be easy – sudar Jun 23 '16 at 19:15
  • @Sudnep yes. Note the settings.foo and settings.bar values, which cause issues on deserialization (wrapped in quotes, backslashes, etc.). – LiquidPony Jun 23 '16 at 19:27

1 Answers1

3

Sorry about the length of the code here. There are plenty of shortcuts here (no encapsulation; added e to defaulte to avoid keyword etc.) but the intent is there Model class:

package com.odwyer.rian.test;

import java.io.IOException;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Model {
    public String id;
    public String name;
    public Connections connections;
    public Settings settings;

    public static class Connections {
        public String defaulte;
        public String dev;

        @Override
        public String toString() {
            return ReflectionToStringBuilder.toString(this);
        }
    }

    public static class Foo {
        public Foo () {}

        @JsonCreator
        public static Foo create(String str) throws JsonParseException, JsonMappingException, IOException {
            return (new ObjectMapper()).readValue(str, Foo.class);
        }

        public Integer fooId;
        public String token;

        @Override
        public String toString() {
            return ReflectionToStringBuilder.toString(this);
        }
    }

    public static class Bar {
        public Bar() {}

        @JsonCreator
        public static Bar create(String str) throws JsonParseException, JsonMappingException, IOException {
            return (new ObjectMapper()).readValue(str, Bar.class);
        }

        public Integer barId;
        public String accountId;

        @Override
        public String toString() {
            return ReflectionToStringBuilder.toString(this);
        }
    }

    public static class Settings {
        public Foo foo;
        public Bar bar;

        @Override
        public String toString() {
            return ReflectionToStringBuilder.toString(this);
        }
    }

    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this);
    }
}

The caller:

package com.odwyer.rian.test;

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class TestClass {
    private static ObjectMapper objectMapper = new ObjectMapper();

    public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
        Scanner file = new Scanner(new File("test.json"));
        String jsonStr = file.useDelimiter("\\Z").next();

        Model model = objectMapper.readValue(jsonStr, Model.class);

        System.out.println(model.toString());
    }
}

The result (too much hassle to format out but it is all there!): com.odwyer.rian.test.Model@190083e[id=abcd1234,name=test,connections=com.odwyer.rian.test.Model$Connections@170d1f3f[defaulte=http://foo.com/api/,dev=http://dev.foo.com/api/v2],settings=com.odwyer.rian.test.Model$Settings@5e7e6ceb[foo=com.odwyer.rian.test.Model$Foo@3e20e8c4[fooId=1,token=abc],bar=com.odwyer.rian.test.Model$Bar@6291bbb9[barId=2,accountId=d7cj3]]]

The key, courtesy of Ted and his post (https://stackoverflow.com/a/8369322/2960707) is the @JsonCreator annotation

Community
  • 1
  • 1
Rian O'Dwyer
  • 435
  • 2
  • 12
  • The use of `@JsonCreator` and `create(String str)` method does correctly parse the JSON, but this solution still requires a per-type implementation. In practice, I have dozens of `Settings` (`foo`, `bar`, `baz`, `batz`, etc.), and ideally the solution would not require modifications to each of those types. – LiquidPony Jun 24 '16 at 15:08
  • You should be able to create a superclass and put the create function there – Rian O'Dwyer Jun 24 '16 at 15:14
  • I considered that but I'm not sure how I would construct an instance of a given subtype in the `create` method of the superclass. – LiquidPony Jun 24 '16 at 17:57
  • 1
    I kind of dislike the use of `new ObjectMapper()` in the static create method annotated with `@JsonCreator`. This means the object mapper used to read the payload is different than the object mapper used to read the internal part of the payload. If you have no configuration that may not be a problem, but many might have a singleton object mapper that they want to use in both locations. Is there no way to get the original objectMapper and reuse it? – gaoagong May 05 '17 at 23:47
  • @gaoagong that is one of the plenty of shortcuts mentioned in my response. You are right of course that a single instance is better, be it injected or accessed via Singleton or Factory pattern – Rian O'Dwyer May 06 '17 at 06:41