Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSON generic collection deserialization

I've such DTO classes written in Java:

public class AnswersDto {
    private String uuid;
    private Set<AnswerDto> answers;
}

public class AnswerDto<T> {
    private String uuid;
    private AnswerType type;
    private T value;
}

class LocationAnswerDto extends AnswerDto<Location> {
}

class JobTitleAnswerDto extends AnswerDto<JobTitle> {
}

public enum AnswerType {
    LOCATION,
    JOB_TITLE,
}

class Location {
    String text;
    String placeId;
}

class JobTitle {
    String id;
    String name;
}

In my project there is Jackson library used for serialization and deserialization of JSONs.

How to configure AnswersDto (use special annotations) or AnswerDto (annotation as well) classes to be able to properly deserialize request with AnswersDto in its body, e.g.:

{
    "uuid": "e82544ac-1cc7-4dbb-bd1d-bdbfe33dee73",
    "answers": [
        {
            "uuid": "e82544ac-1cc7-4dbb-bd1d-bdbfe33dee73",
            "type": "LOCATION",
            "value": {
                "text": "Dublin",
                "placeId": "121"
            }
        },
        {
            "uuid": "e82544ac-1cc7-4dbb-bd1d-bdbfe33dee73",
            "type": "JOB_TITLE",
            "value": {
                "id": "1",
                "name": "Developer"
            }
        }
    ]
}

Unfortunately Jackson by default maps value of AnswerDto object to LinkedHashMap instead of object of proper (Location or JobTitle) class type. Should I write custom JsonDeserializer<AnswerDto> or configuration by use of @JsonTypeInfo and @JsonSubTypes could be enough?

To properly deserialize request with just one AnswerDto in form of

{
    "uuid": "e82544ac-1cc7-4dbb-bd1d-bdbfe33dee73",
    "type": "LOCATION",
    "value": {
        "text": "Dublin",
        "placeId": "121"
    }
}

I'm using:

AnswerDto<Location> answerDto = objectMapper.readValue(jsonRequest, new TypeReference<AnswerDto<Location>>() {
});

without any other custom configuration.

like image 577
Bananan Avatar asked Sep 01 '16 14:09

Bananan


People also ask

How to deserialize a class from a JSON file?

The result is a class that you can use for your deserialization target. 1 Copy the JSON that you need to deserialize. 2 Create a class file and delete the template code. 3 Choose Edit > Paste Special > Paste JSON as Classes . The result is a class that you can use for your deserialization target.

Why can’t Gson deserialize the data field?

But in our case it will fail to deserialize the data field, because Gson simply does not know which parameter type to use. How can we tell Gson about it? There is an alternative signature of the deserialization method—f romJson (String json, Type typeOfT) where Type is java.lang.reflect.

What is the difference between serialization and deserialization of a collection?

Contains elements that are serializable. The serializer calls the GetEnumerator () method, and writes the elements. Deserialization is more complicated and is not supported for some collection types. The following sections are organized by namespace and show which types are supported for serialization and deserialization. * See Supported key types.

What is the alternative signature of deserialization method in Java?

There is an alternative signature of the deserialization method—f romJson (String json, Type typeOfT) where Type is java.lang.reflect. Type which may contain information about parametrization too. One of the ways to extract the type from parametrized class is to use Gson's TypeToken class, which uses reflection magic in order to do this.


2 Answers

I've resolved issue by using Jackson's custom annotations @JsonTypeInfo and @JsonSubTypes:

public class AnswerDto<T> {

    private String uuid;

    private AnswerType type;

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type")
    @JsonSubTypes({
            @JsonSubTypes.Type(value = Location.class, name = AnswerType.Types.LOCATION),
            @JsonSubTypes.Type(value = JobTitle.class, name = AnswerType.Types.JOB_TITLE)
    })
    private T value;
}
like image 198
Bananan Avatar answered Oct 27 '22 09:10

Bananan


My suggestion is to make a separate interface for possible answer values and use @JsonTypeInfo on it. You can also drop type field from AnswerDto, AnswerType enum, and additional *AnswerDto classes becuse jackson will add type info for you. Like this

public class AnswerDto<T extends AnswerValue> {
    private String uuid;
    private T value;
}

@JsonTypeInfo(use = Id.CLASS, include = As.PROPERTY)
interface AnswerValue {}

class Location implements AnswerValue { /*..*/ }
class JobTitle implements AnswerValue { /*..*/ }

Resulting json will looks like this

{
  "uuid": "e82544ac-1cc7-4dbb-bd1d-bdbfe33dee73",
  "answers": [
    {
      "uuid": "e82544ac-1cc7-4dbb-bd1d-bdbfe33dee73",
      "value": {
        "@class": "com.demo.Location",
        "text": "Dublin",
        "placeId": "121"
      }
    },
    {
      "uuid": "e82544ac-1cc7-4dbb-bd1d-bdbfe33dee73",
      "value": {
        "@class": "com.demo.JobTitle",
        "id": "1",
        "name": "Developer"
      }
    }
  ]
}

Which will be parsed using

AnswersDto answersDto = objectMapper.readValue(json, AnswersDto.class);

But this solution applies only in cases when you are a producer of json data and you do not have to think about backward compatibility.

In other cases you'll have to make custom desetializer for AnswersDto class.

like image 22
vsminkov Avatar answered Oct 27 '22 10:10

vsminkov