Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse nested json into flat model class objects #2555

Open
mpsingh47 opened this issue Nov 25, 2023 · 1 comment
Open

Parse nested json into flat model class objects #2555

mpsingh47 opened this issue Nov 25, 2023 · 1 comment

Comments

@mpsingh47
Copy link

Problem solved by the feature

No need of creating nested classes according to the json file.

Feature description

if i have a nested json like :
{ id : 1 ,
name : "myname" ,
address : {
street : "my lane" ,
city : "mycity" }
}
I want to parse this json into target model class but cannot parse without creating nested class 'address' inside my target class.
There should be a method to write like @serialized("address.street") in model class field which automatically get the nested street value from address so that i do not need to create a nested class in order to parse this json into object .
Target model class will be Model> id, name, street, city

Alternatives / workarounds

alternative is simply create nested class Model> id , name, Address> street, city

@Marcono1234
Copy link
Collaborator

Marcono1234 commented Nov 26, 2023

For the example you provided, you could solve this with a custom TypeAdapterFactory, which relies on @SerializedName to indicate flattened names (or rather it does not directly rely on the @SerializedName annotation, but on the name it produces):

Flattening type adapter proof-of-concept (click to expand)
class FlatteningAdapterFactory implements TypeAdapterFactory {
  // Called by Gson
  public FlatteningAdapterFactory() {}

  @Override
  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    TypeAdapter<JsonObject> jsonObjectAdapter = gson.getAdapter(JsonObject.class);
    TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, type);

    return new TypeAdapter<T>() {
      private final char separator = '.';

      private void flattenInto(String name, JsonObject toFlatten, JsonObject destination) {
        for (Map.Entry<String, JsonElement> entry : toFlatten.entrySet()) {
          String flattenedName = name + separator + entry.getKey();
          if (destination.has(flattenedName)) {
            throw new IllegalArgumentException("Duplicate name: " + flattenedName);
          }
          destination.add(flattenedName, entry.getValue());
        }
      }

      @Override
      public T read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
          in.skipValue();
          return null;
        }

        /*
         * Flattens nested JsonObject values, e.g.:
         *   {
         *     "a": 1,
         *     "b": {
         *       "x": true,
         *       "y": 2
         *     }
         *   }
         * Becomes
         *   {
         *     "a": 1,
         *     "b.x": true,
         *     "b.y": 2
         *   }
         */

        JsonObject jsonObject = jsonObjectAdapter.read(in);
        JsonObject flattened = new JsonObject();

        for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
          String name = entry.getKey();
          JsonElement value = entry.getValue();

          // Flatten the value
          if (value instanceof JsonObject) {
            flattenInto(name, (JsonObject) value, flattened);
          }

          // But also add the non-flattened value in case this entry should not actually be flattened
          // The delegate adapter will then ignore either the flattened or the non-flattened entries
          if (flattened.has(name)) {
            throw new IllegalArgumentException("Duplicate name: " + name);
          }
          flattened.add(name, value);
        }

        // Now read the flattened JsonObject using the delegate adapter
        return delegateAdapter.fromJsonTree(flattened);
      }

      @Override
      public void write(JsonWriter out, T value) throws IOException {
        if (value == null) {
          out.nullValue();
          return;
        }

        /*
         * Expands the flattened JsonObject, e.g.:
         *   {
         *     "a": 1,
         *     "b.x": true,
         *     "b.y": 2
         *   }
         * Becomes
         *   {
         *     "a": 1,
         *     "b": {
         *       "x": true,
         *       "y": 2
         *     }
         *   }
         */

        JsonObject flattened = (JsonObject) delegateAdapter.toJsonTree(value);
        JsonObject expanded = new JsonObject();
        Map<String, JsonElement> expandedAsMap = expanded.asMap();

        for (Map.Entry<String, JsonElement> entry : flattened.entrySet()) {
          String name = entry.getKey();
          JsonElement entryValue = entry.getValue();

          // Expand the flattened entry
          int separatorIndex = name.indexOf(separator);
          if (separatorIndex != -1) {
            String namePrefix = name.substring(0, separatorIndex);
            String nameSuffix = name.substring(separatorIndex + 1);
            JsonObject nestedObject =
                (JsonObject) expandedAsMap.computeIfAbsent(namePrefix, k -> new JsonObject());

            if (nestedObject.has(nameSuffix)) {
              throw new IllegalArgumentException("Duplicate name: " + nameSuffix);
            }
            nestedObject.add(nameSuffix, entryValue);
          } else {
            if (expanded.has(name)) {
              throw new IllegalArgumentException("Duplicate name: " + name);
            }
            expanded.add(name, entryValue);
          }
        }

        // Finally write the expanded JsonObject to the actual writer
        jsonObjectAdapter.write(out, expanded);
      }
    };
  }
}

Usage:

@JsonAdapter(FlatteningAdapterFactory.class)
class MyClass {
  int id;
  String name;

  @SerializedName("address.street")
  String street;
  @SerializedName("address.city")
  String city;

  @Override
  public String toString() {
    return "MyClass [id=" + id + ", name=" + name + ", street=" + street + ", city=" + city + "]";
  }
}
Gson gson = new Gson();

MyClass deserialized = gson.fromJson(
    "{\"id\":1,\"name\":\"my name\",\"address\":{\"street\":\"my lane\",\"city\":\"my city\"}}",
    MyClass.class
);
System.out.println(deserialized);

String json = gson.toJson(deserialized);
System.out.println(json);

I haven't tested this extensively though, it might not be very performant, and it might not work well for more complex cases.
But maybe it is useful for you nonetheless.

There are also multiple Stack Overflow questions and answers regarding how to flatten model classes, which could maybe be used as solution here as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants