Code Bug Fix: Serialize list of objects, keyed by attribute

Original Source Link

Say I have a class:

public class Person {
   String name;
   Int age;
}

and a list of objects of this class:

List<Person> people = ...

Normally, running this through a serializer such as Jackson or Gson would result in this:

"[{'name':'John','age':42},{'name':'Sam','age':43}]

but I am looking to serialize to a single json object where each property is a list containing the attributes, like this:

"{'name':['John','Sam'],'age':[42,43]}"

Do any of the serialization libraries support this?

I’d create a sort of “wrapper” that takes in any amount of persons and stores the fields in a way that let them be serialized that way. So in this case, you would create a series of persons, create a wrapper containing those persons and then serialize that.

public class PersonWrapper {

    private int[] ages;
    private String[] names;

    public PersonWrapper(Person... persons) {
        ages = new int[persons.length];
        names = new String[persons.length];

        for (int i = 0; i < persons.length; i++) {
            ages[i] = persons[i].getAge();
            names[i] = persons[i].getName();
        }
    }
}

Transform your List<Person> to new object.

class NewClass {
    List<String> name;
    List<Integer> ages;
}

Then pass this object through the Serializer to get:
"{'name':['John','Sam'],'age':[42,43]}"

Serialization libraries are generally not designed for stuff like that.
What you’re looking for is JSON tree transformation and you can easily implement it in both Gson and Jackson.

Here is a transformation example for Gson:

final class Transformations {

    private Transformations() {
    }

    static JsonObject transposeShallow(final Iterable<? extends JsonElement> inArray) {
        final JsonObject outObject = new JsonObject();
        for ( final String name : scanAllNames(inArray) ) {
            final JsonArray outSubArray = new JsonArray();
            for ( final JsonElement inJsonElement : inArray ) {
                if ( !inJsonElement.isJsonObject() ) {
                    throw new IllegalArgumentException(inJsonElement + " is not a JSON object");
                }
                outSubArray.add(inJsonElement.getAsJsonObject().get(name));
            }
            outObject.add(name, outSubArray);
        }
        return outObject;
    }

    private static Iterable<String> scanAllNames(final Iterable<? extends JsonElement> jsonArray) {
        final ImmutableSet.Builder<String> allNames = new ImmutableSet.Builder<>();
        for ( final JsonElement jsonElement : jsonArray ) {
            if ( !jsonElement.isJsonObject() ) {
                throw new IllegalArgumentException(jsonElement + " is not a JSON object");
            }
            allNames.addAll(jsonElement.getAsJsonObject().keySet());
        }
        return allNames.build();
    }

}

The transformations above can be incorporated into serialization process, but this would affect the structure for your objects (e.g. how to indicate transposable collections for root objects and their fields? how to introduce a new “transposing” type and incorporate it in your data model type system? how to deserialize it properly if necessary?).

Once you get a JSON tree, you can transpose it on any nesting level.
Transposing the root element is the simplest way as long as you don’t need to transform nested elements.

private static final Type peopleType = new TypeToken<Collection<Person>>() {}.getType();

public static void main(final String... args) {
    System.out.println(gson.toJson(people, peopleType));
    System.out.println(gson.toJson(Transformations.transposeShallow(gson.toJsonTree(people, peopleType).getAsJsonArray())));
}

that gives:

[{"name":"John","age":42},{"name":"Sam","age":43}]
{"name":["John","Sam"],"age":[42,43]}

The solution above can work with almost any types in your code, but is has a small penalty for building a JSON tree.
Something like PersonWrapper as suggested by Schred might be another option but this requires wrappers for all your types and they need to be updated once your wrapped classes change.

Also, you might also be interested in libraries like Jolt that are designed for JSON transformations (not sure if it’s doable in Jolt though).

Tagged : / / / /

Code Bug Fix: POJO to Map with Gson with custom deserializer

Original Source Link

I have a simple POJO class looking like this:

public class EventPOJO {

    public EventPOJO() {
    }

    public String id, title;
    // Looking for an annotation here
    public Timestamp startDate, endDate;
}

I have an instance of EventPOJO and need to deserialize this instance into a Map<String, Object> object.

Gson gson = new GsonBuilder().create();
String json = gson.toJson(eventPOJO);
Map<String,Object> result = new Gson().fromJson(json, Map.class);

I need the result Map to contain the key startDate with the value of Type Timestamp.
(com.google.firebase.Timestamp)

Instead however result contains the key startDate with the value of Type LinkedTreeMap containing the nanoseconds and seconds.

I tried creating a custom deserializer:

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Timestamp.class, new TimestampDeserializer());
Gson gson = gsonBuilder.create();
String json = gson.toJson(eventPOJO);
Map<String,Object> result = gson.fromJson(json, Map.class);

TimestampDeserializer.java

public class TimestampDeserializer implements JsonDeserializer<Timestamp> {
    @Override
    public Timestamp deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        //not sure how to retrieve the seconds from the parameters
        return new Timestamp(999999999, 0);
    }
}

The deserialize never even gets called and I am still getting a Map with no Timestamp objects.

If you need a result as Map, then why not simply create a Map without using gson:

public class EventPOJO {

    public EventPOJO() {
    }

    public EventPOJO(String id) {
        this.id = id;
    }

    public String id, title;
    // Looking for an annotation here
    public Timestamp startDate, endDate;

    public Map<String, Object> getMap() {
        Map<String, Object> res = new HashMap<>();
        res.put("id", id);
        res.put("title", title);
        res.put("startDate", startDate);
        res.put("endDate", endDate);
        return res;
    }
}

Then call

Map<String, Object> result = eventPOJO.getMap();

Tagged : /

Code Bug Fix: Using default JsonDeserializer inside custom JsonDeserializer in Kotlin

Original Source Link

I’m trying to build custom deserializers for the responses I get from OMDb API.

Here’s the data class for Movie:

data class Movie(
    val title: String?,
    val year: String?,
    val imdbID: String?,
    val type: String?,
    val poster: String?,
    val mpaRating: String?,
    val runtime: String?,
    val genres: String?,
    val director: String?,
    val writers: List<String>?,
    val actors: List<String>?,
    val plot: String?,
    val awards: String?,
    val boxOfficeEarnings: String?,
    val ratings: List<Rating>,
    val response: Boolean?
)

And for Rating:

data class Rating(
    @SerializedName("Source")
    val source: String,
    @SerializedName("Value")
    val value: String
)

This is the custom JsonDeserializer so far:

class MovieDeserializer : JsonDeserializer<Movie>
{
    override fun deserialize(
        json: JsonElement?,
        typeOfT: Type?,
        context: JsonDeserializationContext?
    ): Movie
    {
        val movieJsonObject = json?.asJsonObject
        return Movie(
            movieJsonObject?.get("Title")?.asString,
            movieJsonObject?.get("Year")?.asString,
            movieJsonObject?.get("imdbID")?.asString,
            movieJsonObject?.get("Type")?.asString,
            movieJsonObject?.get("Poster")?.asString,
            movieJsonObject?.get("Rated")?.asString,
            movieJsonObject?.get("Runtime")?.asString,
            movieJsonObject?.get("Genre")?.asString,
            movieJsonObject?.get("Director")?.asString,
            separateStringByComma(movieJsonObject?.get("Writer")?.asString),
            separateStringByComma(movieJsonObject?.get("Actors")?.asString),
            movieJsonObject?.get("Plot")?.asString,
            movieJsonObject?.get("Awards")?.asString,
            movieJsonObject?.get("BoxOffice")?.asString,
            // this is where I need help,
            movieJsonObject?.get("Response")?.asBoolean
        )
    }

    fun separateStringByComma(stringToSeparate: String?): List<String>?
    {
        return stringToSeparate?.split(", ")
    }
}

How can I convert that JsonElement directly to List<Rating> without some json string manipulation?

By the way, I’m using Retrofit with Gson:

val gsonMovieDeserializer = GsonBuilder()
            .registerTypeAdapter(Movie::class.java, MovieDeserializer())
            .create()
        val retrofit = Retrofit.Builder()
            .baseUrl("https://www.omdbapi.com/")
            .addConverterFactory(GsonConverterFactory.create(gsonMovieDeserializer))
            .build()
        val omdbApi = retrofit.create(OmdbApi::class.java)
        val movie = omdbApi.getMovie(movieImdbId.value.toString())

First of all, I’d like to point the usage of nullables there: instead of checking wheter movieJsonObject is null or not for every call inside deserialize(), you should change the function parameters not to be null and then check only once, right at the beggining, if json is a JsonObject, just skipping everything if it’s not. That way, we have a solid base to extract the data. Also, for the Movie data class, check the API documentation for which fields are optional and only set those to nulalble (I’m pretty sure at least the title and ID there are always present, so it’s way more useful to have them as non-nullable).

Now, for the question itself, you should probably be able to deserialize that list using context.deserialize<List<Rating>>(movieJsonObject.get("Ratings"), List::class.java), which, in Kotlin, will return a type-safe List<Rating> (but, again, make sure that’s not an optional field in the API and, if it is, make it nullable).

Tagged : / / / /