4

I'm writing a network class and want to be able to parse different responses to different classes (there's still one-to-one relationship but I want to have a single parseResponse() that will deal with all responses from different endpoints, and endpoint.className has the expected classType that I should map to):

private Class<?> parseResponse(StringBuilder responseContent, Endpoint endpoint) {
    ObjectMapper mapper = new ObjectMapper();
    try {
        Class<?> object = mapper.readValue(responseContent.toString(), endpoint.className);
        // endpoint.className has Class<?> type
        if (object instanceof endpoint.className) {
        }
    } catch (IOException e) {
        // handle errors
    }
}

But there's an error if I write if (object instanceof endpoint.className)

Update: probably the better option is to add parse() method to Endpoint class:

public Class<?> parseResponse(String responseContent) {
   // this.className has Class<?> type (e.g., Foo.class).
}

public enum Endpoint {
    FOO (Foo.class),
    BAR (Bar.class);

    private Class<?> classType;
}

But there're still the same type errors.

B. Wasnie
  • 541
  • 1
  • 4
  • 12
  • 1
    Don't do this - it's violating Open Closed principle – m.antkowicz Aug 20 '19 at 21:41
  • 1
    Shall I create a separate `parse()` for each class separately (like create an interface or something)? – B. Wasnie Aug 20 '19 at 21:48
  • yeah probably that's what you should do – m.antkowicz Aug 20 '19 at 21:49
  • @m.antkowicz can I invoke `className.parse()` then? – B. Wasnie Aug 20 '19 at 21:58
  • just as an FYI `Class>` is a java class itself so an instance of it (like the method return value or 'object' variable in your example) will be of type `Class>` class, not your '?' class. – Aarjav Aug 20 '19 at 22:13
  • @Aarjav oh I see, how can I access `?` then? I think my questions should be really popular but I couldn't google anything similar. – B. Wasnie Aug 20 '19 at 22:15
  • Since you're already using jackson you may want to read through [this](https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization) and see if it applies in your case. Ideally you'd have a limited set of classes you want to deserialize to and in that case you may be able to use an interface or similar and use the `@JsonTypeInfo` annotation – Aarjav Aug 20 '19 at 22:15
  • I do have a limited set of classes I want to deserialize to. Could you give me a code pointer or sth to read about what interface shall I use? – B. Wasnie Aug 20 '19 at 22:17
  • You may need to store and return `Object` or ask for a type parameter in java (`public T parseResponse(String json)`) – Aarjav Aug 20 '19 at 22:17
  • https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization – Aarjav Aug 20 '19 at 22:19
  • I'm still a bit confused, could you point me to the minimal code sample? FYI there's no inheritance in my responses at all. I won't find any `type` fields in my response either. – B. Wasnie Aug 20 '19 at 22:20
  • 1
    Take a look at [Deserializing or serializing any type of object using Jackson ObjectMapper and handling exceptions](https://stackoverflow.com/questions/56299558/deserializing-or-serializing-any-type-of-object-using-jackson-objectmapper-and-h/56299668). You can not declare a method and return `Class>` because you need an instance of object not it class. You should create two different methods for `Foo` and `Bar`: `public Foo parseResponseF(String responseContent)` and `public Bar parseResponseB(String responseContent)` like in related answer there is a method: `deserialiseToSomeSpecificClass` – Michał Ziober Aug 20 '19 at 22:28
  • @MichałZiober sounds great! I've got a question though: how do I invoke Foo's `parseResponseF()/parseResponseB()` method? – B. Wasnie Aug 20 '19 at 22:31
  • 1
    @B.Wasnie, you can not have one `parseResponse` method. You need one method for a one type. You need to inform `Jackson` which type do you need. So, in case you want to parse `Foo` you invoke `parseResponseFoo` which is declared `public Foo parseResponseFoo(String responseContent)` and for `Bar` you need the same. So, you have two methods which knows a type. You can not implement it like: `public T parseResponse(String payload) { return jsonMapper.deserialise(payload, T.class); }` – Michał Ziober Aug 20 '19 at 22:38
  • @MichałZiober oh I see, basically I'll have `object instanceof Foo` -> call parseResponseF()` and `object instanceof Bar` -> call parseResponseB()`, basically I'll have a switch case construction, right? – B. Wasnie Aug 20 '19 at 22:53
  • 1
    @B.Wasnie, you do not need to check result instance because it will be a type which you provided. See, my answer which shows how you should solve your problem. Your do not even need `Endpoint` enum. You can declare all methods you need directly. – Michał Ziober Aug 20 '19 at 23:04

1 Answers1

1

You should separate JSON deserialisation from other parts of your app. You can not implement one method for all responses but you probably have a limited number of responses and you can declare some simple methods for each class. Generally, you could have only one method with declaration like below:

public <T> T deserialise(String payload, Class<T> expectedClass) {
    Objects.requireNonNull(payload);
    Objects.requireNonNull(expectedClass);

    try {
        return mapper.readValue(payload, expectedClass);
    } catch (IOException e) {
        throw new IllegalStateException("JSON is not valid!", e);
    }
} 

And now, you can deserialise all payloads you want. You need to provide JSON payload and POJO class you want to receive back.

Simple working solution which shows that concept:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

import java.io.IOException;
import java.util.Objects;

public class JsonMapper {

    private final ObjectMapper mapper = new ObjectMapper();

    public JsonMapper() {
        // configure mapper instance if required
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
        // etc...
    }

    public String serialise(Object value) {
        try {
            return mapper.writeValueAsString(value);
        } catch (JsonProcessingException e) {
            throw new IllegalStateException("Could not generate JSON!", e);
        }
    }

    public <T> T deserialise(String payload, Class<T> expectedClass) {
        Objects.requireNonNull(payload);
        Objects.requireNonNull(expectedClass);

        try {
            return mapper.readValue(payload, expectedClass);
        } catch (IOException e) {
            throw new IllegalStateException("JSON is not valid!", e);
        }
    }

    public Foo parseResponseFoo(String payload) {
        return deserialise(payload, Foo.class);
    }

    public Bar parseResponseBar(String payload) {
        return deserialise(payload, Bar.class);
    }

    public static void main(String[] args) {
        JsonMapper jsonMapper = new JsonMapper();

        String bar = "{\"bar\" : 2}";
        System.out.println(jsonMapper.parseResponseBar(bar));

        String foo = "{\"foo\" : 1}";
        System.out.println(jsonMapper.parseResponseFoo(foo));

        System.out.println("General method:");
        System.out.println(jsonMapper.deserialise(foo, Foo.class));
        System.out.println(jsonMapper.deserialise(bar, Bar.class));
    }
}

class Foo {

    public int foo;

    @Override
    public String toString() {
        return "Foo{" +
                "foo=" + foo +
                '}';
    }
}

class Bar {

    public int bar;

    @Override
    public String toString() {
        return "Bar{" +
                "bar=" + bar +
                '}';
    }
}

See also:

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • It'll definitely work but I think the question was more about how to avoid passing hardcoded Foo.class and pass it as an associated value with an enum instead. – James Larkin Aug 21 '19 at 06:05
  • @JamesLarkin, enum `with` class inside adds only extra level which does not provide value. For each class you want to deserialise you need to create new enum value. Instead of providing hardcoded `Foo.class` we need to provide `Endpoint.Foo` which is also hardcoded. To make it clear and straightforward I suggested to create method for each class and we can avoid providing class at all. If you really want to use enum you can create new `JsonMapperEndpoint` which gets enum and in switch invokes right method. But in this case we need casting... – Michał Ziober Aug 21 '19 at 07:33