Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert nested JSON to nested Java object using gson TypeAdapter

I am trying to use google gson TypeAdapter for converting nested JSON into nested Java object having implementation of TypeAdapter for each class. But I don't want to write complete read() method logic in single adapter class. I have referred few questions and blog examples over net. But complete read logic is in single class.

For small nested object its fine to have logic in single Adapter but for big object (having more than 10-15 fields in each class) it's not good.

[Update]

For example json keys look same as of class attributes, but in actual I will be getting input as hyphen-separated-small-case keys instead of Camel case keys. So my json and java classes attribute names will not be same hence I have to write my custom logic for mapping.

E.g. Sample Json input :

{
  "id": 1,
  "name": "Alex",
  "emailId": "[email protected]",
  "address": {
    "address": "21ST & FAIRVIEW AVE",
    "district": "district",
    "city": "EATON",
    "region": "PA",
    "postalCode": "18044",
    "country": "US"
  }
}

And Java beans are as below :

//Employee object class
public class Employee {

  private int id;
  private String name;
  private String emailId;
  private Address address;
  ..
}

//Address object class
public class Address {

  private String address;
  private String district;
  private String city;
  private String region;
  private String postalCode;
  private String country;
  ..
}

I want to have two different adapters and integrate multiple adapters in read() method.

public class EmployeeAdapter extends TypeAdapter<Employee> {
  @Override
  public void write(JsonWriter out, Employee employee) throws IOException {
    //
  }

  @Override
  public Employee read(JsonReader jsonReader) throws IOException {
    //read logic for employee class using AddressAdapter for address json
  }
}

public class AddressAdapter extends TypeAdapter<Address> {
  @Override
  public void write(JsonWriter out, Address address) throws IOException {
    //
  }

  @Override
  public Address read(JsonReader jsonReader) throws IOException {
    //read logic for Address class
  }
}

How can I use AddressAdapter inside EmployeeAdapter?

like image 450
GovindS Avatar asked Mar 07 '17 09:03

GovindS


2 Answers

I use a TypeAdapterFactory for this kind of thing. It allows passing the gson instance to the TypeAdapter instance.

(In the example below, I left in passing "rawType" to the TypeAdapter instance, since its often useful. Pull that out if not needed.)

Example TypeAdapterFactory:

public class ContactTypeAdapterFactory implements TypeAdapterFactory {

    // Add @SuppressWarnings("unchecked") as needed.

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        final Class<? super T> rawClass = typeToken.getRawType();
        if (Employee.class.isAssignableFrom(rawClass)) {
            // Return EmployeeAdapter for Employee class
            return EmployeeAdapter.get(rawClass, gson);
        }
        if (Address.class.isAssignableFrom(rawClass)) {
            // Return AddressAdapter for Address class
            return AddressAdapter.get(rawClass, gson);
        }
        return null; // let Gson find somebody else
    }

    private static final class EmployeeAdapter<T> extends TypeAdapter<T> {

        private final Gson gson;
        private final Class<? super T> rawClass;  // Not used in this example

        private EmployeeAdapter(Class<? super T> rawClass, Gson gson) {
            this.rawClass = rawClass;
            this.gson = gson;
        }

        private static <T> TypeAdapter<T> get(Class<? super T> rawClass, Gson gson) {
            // Wrap TypeAdapter in nullSafe so we don't need to do null checks
            return new EmployeeAdapter<>(rawClass, gson).nullSafe();
        }

        @Override
        public void write(JsonWriter out, T value)
                throws IOException {

            // We should only ever be here for Employee types
            // Cast value to Employee
            Employee record = (Employee)value;

            // Output start of JSON object
            out.beginObject();

            // Output key / value pairs
            out.name("name");
            gson.getAdapter(String.class).write(out, record.getName());
            // [...]
            out.name("address");
            gson.getAdapter(Address.class).write(out, record.getAddress());

            // Output end of JSON object
            out.endObject();
        }

        @Override
        public T read(JsonReader in)
                throws IOException {

            String fieldName;

            // Create an empty Employee object
            Employee record = new Employee();

            // Consume start of JSON object
            in.beginObject();

            // Iterate each key/value pair in the json object
            while (in.hasNext()) {
                fieldName = in.nextName();
                switch (fieldName) {
                    case "name":
                        record.setName(gson.getAdapter(String.class).read(in));
                        break;
                    // [...] 
                    case "address":
                        record.setAddress(gson.getAdapter(Address.class).read(in));
                        break;
                    default:
                        // Skip any values we don't support
                        in.skipValue();
                }
            }
            // Consume end of JSON object
            in.endObject();

            // Return new Object
            return (T)record;
        }

    }

    private static final class AddressAdapter<T> extends TypeAdapter<T> {

        private final Gson gson;
        private final Class<? super T> rawClass; // Not used in this example

        private AddressAdapter(Class<? super T> rawClass, Gson gson) {
            this.rawClass = rawClass;
            this.gson = gson;
        }

        private static <T> TypeAdapter<T> get(Class<? super T> rawClass, Gson gson) {
            // Wrap TypeAdapter in nullSafe so we don't need to do null checks
            return new AddressAdapter<>(rawClass, gson).nullSafe();
        }

        @Override
        public void write(JsonWriter out, T value)
                throws IOException {

            // We should only ever be here for Address types
            // Cast value to Address
            Address record = (Address)value;

            // Output start of JSON object
            out.beginObject();

            // Output key / value pairs
            out.name("address");
            gson.getAdapter(String.class).write(out, record.getName());
            // [...]
            out.name("country");
            gson.getAdapter(String.class).write(out, record.getCountry());

            // Output end of JSON object
            out.endObject();
        }

        @Override
        public T read(JsonReader in)
                throws IOException {

            String fieldName;

            Address record = new Address();
            in.beginObject();
            // Iterate each parameter in the json object
            while (in.hasNext()) {
                fieldName = in.nextName();
                switch (fieldName) {
                    case "address":
                        record.setAddress(gson.getAdapter(String.class).read(in));
                        break;
                    // [...]    
                    case "country":
                        record.setCountry(gson.getAdapter(String.class).read(in));
                        break;
                    default:
                        in.skipValue();
                }
            }
            in.endObject();
            return (T)record;

        }

    }

}

Use:

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(new ContactTypeAdapterFactory())
    .create();
Employee employee = gson.fromJson(jsonString, Employee.class);
like image 53
JohnK Avatar answered Oct 19 '22 17:10

JohnK


I had the same issue and found a suitable solution for me.

You can get a new TypeAdapter<T> instance with help of a Gson object and its method getAdapter(Class<T> type).

So your provided example would look like this:

Java Beans:

//Employee object class
@JsonAdapter(EmployeeAdapter.class)
public class Employee {

  private int id;
  private String name;
  private String emailId;
  private Address address;
  ..
}

//Address object class
@JsonAdapter(AddressAdapter.class)
public class Address {

  private String address;
  private String district;
  private String city;
  private String region;
  private String postalCode;
  private String country;
  ..
}

Type Adapters:

public class EmployeeAdapter extends TypeAdapter<Employee> {
  @Override
  public void write(JsonWriter out, Employee employee) throws IOException {
    //
  }

  @Override
  public Employee read(JsonReader jsonReader) throws IOException {
    Employee employee = new Employee();

    jsonReader.beginObject();
    //read your Employee fields

    TypeAdapter<Address> addressAdapter = new Gson().getAdapter(Address.class);
    employee.setAddress(addressAdapter.read(jsonReader);

    return employee;
  }
}

public class AddressAdapter extends TypeAdapter<Address> {
  @Override
  public void write(JsonWriter out, Address address) throws IOException {
    //
  }

  @Override
  public Address read(JsonReader jsonReader) throws IOException {
    Address address = new Address();
    //read your Address fields
    return address;
  }
}

With this solution you have the benefits of a loosely coupled code, because of the only dependency in the Beans JsonAdapter annotation.
Addtional you split the read / write logic for each Bean to its own TypeAdapter.

like image 23
Dennis Avatar answered Oct 19 '22 18:10

Dennis