9

I'm using Google GSON to transform my Java object to JSON.

Currently I'm having the following structure:

"Step": {
  "start_name": "Start",
  "end_name": "End",
  "data": {
    "duration": {
      "value": 292,
      "text": "4 min."
    },
    "distance": {
       "value": 1009.0,
       "text": "1 km"
    },
    "location": {
       "lat": 59.0000,
       "lng": 9.0000,
       "alt": 0.0
    }
  }
}

Currently a Duration object is inside a Data object. I would like to skip the Data object and move the Duration object to the Step object, like this:

"Step": {
  "start_name": "Start",
  "end_name": "End",
  "duration": {
    "value": 292,
    "text": "4 min."
  },
  "distance": {
     "value": 1009.0,
     "text": "1 km"
  },
  "location": {
     "lat": 59.0000,
     "lng": 9.0000,
     "alt": 0.0
  }
}

How can I do this using GSON?

EDIT: I've tried to use a TypeAdapter to modify the Step.class, but in the write-method I'm not able to add my duration object to the JsonWriter.

dhrm
  • 14,335
  • 34
  • 117
  • 183

4 Answers4

8

You can probably do this by writing, and then registering a custom serializer for Step, and making sure inside it you work with Duration etc, instead of Data.

// registering your custom serializer:
GsonBuilder builder = new GsonBuilder ();
builder.registerTypeAdapter (Step.class, new StepSerializer ());
Gson gson = builder.create ();
// now use 'gson' to do all the work

The code for the custom serializer below, I'm writing off the top of my head. It misses exception handling, and might not compile, and does slow things like create instances of Gson repeatedly. But it represents the kind of thing you'll want to do:

class StepSerializer implements JsonSerializer<Step>
{
  public JsonElement serialize (Step src,
                                Type typeOfSrc,
                                JsonSerializationContext context)
    {
      Gson gson = new Gson ();
      /* Whenever Step is serialized,
      serialize the contained Data correctly.  */
      JsonObject step = new JsonObject ();
      step.add ("start_name", gson.toJsonTree (src.start_name);
      step.add ("end_name",   gson.toJsonTree (src.end_name);

      /* Notice how I'm digging 2 levels deep into 'data.' but adding
      JSON elements 1 level deep into 'step' itself.  */
      step.add ("duration",   gson.toJsonTree (src.data.duration);
      step.add ("distance",   gson.toJsonTree (src.data.distance);
      step.add ("location",   gson.toJsonTree (src.data.location);

      return step;
    }
}
ArjunShankar
  • 23,020
  • 5
  • 61
  • 83
  • I only need to `serialize`, so `deserialize` is not important. The above was just an example of my structure. In fact my data-object contains additional fields, which needs to be included in the `toJsonTree`. Any clue how I can do that? – dhrm Jun 05 '12 at 15:43
  • This *really* depends. Show an example with two additional fields and I'll see what I can think of. Caveat: My answer is based on Gson documentation. I've never done weird stuff like this before. – ArjunShankar Jun 05 '12 at 16:32
  • I've updated my question with additional fields inside the data-object. – dhrm Jun 05 '12 at 16:41
  • @DennisMadsen - I've updated my answer to reflect your change. Good luck. – ArjunShankar Jun 05 '12 at 17:04
  • Thanks. Is there a way to create the `step` variable from the src Step and just remove it's data object? If that's possible I wouldn't have to add the start and end name again. – dhrm Jun 05 '12 at 17:22
  • @DennisMadsen - There is a way to do that using [`remove`](http://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/JsonObject.html#remove(java.lang.String)). I highly recommend the rest of the API documentation for JsonObject as well. – ArjunShankar Jun 05 '12 at 17:55
  • @DennisMadsen - Did you actually implement the serializer and see that it works for you, before marking this answer correct? I'm not too sure because everything I've written is based on Gson documentation. I haven't tested a single line of code I wrote, although the *hope* is that it is fairly correct. – ArjunShankar Jun 05 '12 at 17:57
  • Yes, I did. It almost worked except from the fact, that I've created a `LocationAdapterFactory` which modify all the locations in my JSON tree, but when I manually add a location, it seems that this isn't changed by my `LocationAdapterFactory`. – dhrm Jun 06 '12 at 06:42
3

In such case I register TypeAdapter for nested data field. Within the the adapter data's fields are added to parent object. No need to create adapter for enclosing class.

public class Step {
    private String startName;
    private endName;
    @JsonAdapter(JsonFlatMapAdapter.class)
    private Map<String, Object> data;
    ...
}

public class JsonFlatMapAdapter extends TypeAdapter<Map<String, Object>> {

    @Override
    public void write(JsonWriter out, Map<String, Object> value) throws IOException {
        out.nullValue();
        Gson gson = new Gson();
        value.forEach((k,v) -> {
            try {
                out.name(k).jsonValue(gson.toJson(v));
            } catch (IOException e) {
            }
        });
    }

    @Override
    public Map<String, Object> read(JsonReader in) throws IOException {
        return null;
    }

}
1

Ran into the same problem. The answer from @ArjunShunkar pointed me in the right direction. I fixed it writing a custom Serializer, but slightly different:

public class StepSerializer implements JsonSerializer<Step> {
    @Override
    public JsonElement serialize(Step src, Type typeOfSrc, JsonSerializationContext context) {
        JsonObject step = new JsonObject();
        step.add ("start_name", context.serialize(src.start_name);
        step.add ("end_name",   context.serialize(src.end_name);

        JsonObject data = context.serialize(src.data).getAsJsonObject();
        data.entrySet().forEach(entry -> {
            step.add(entry.getKey(), entry.getValue());
        });

        return step;
    }
}

This could be improved further, the "start_name" and "end_name" props are still hardcoded. Could be removed by going over the entrySet for the root object and excluding 'data' there, leaving only the element that needs to be unwrapped hardcoded.

TiemenSchut
  • 256
  • 1
  • 5
0

I don't think there is beautifull way to do it in gson. Maybe get java object (Map) from initial json, remove data, put duration and serialize to json:

Map initial = gson.fromJson(initialJson);

// Replace data with duration in this map
Map converted = ...

String convertedJson = gson.toJson(converted);
alexey28
  • 5,170
  • 1
  • 20
  • 25