简体   繁体   中英

Retrofit and GSON to parse an Array of objects with no names

I got the JSON response like this:

{
    "USA": [
        "New York",
        "Texas",
        "Hawaii"
    ],
    "Turkey": [
        "Ankara",
        "Istanbul"
    ],
    "Lebanon": [
        "Beirut",
        "Zahle"
    ]
}

I want to get this as

public class Country {
    private String name = null;
    private List<String> cities = null;
}

How can we parse the JSON objects when they do not have names like

{
    "name": "USA",
    "cities": [
              "New York",
              .... etc
}

? Thank you in advance.

Looks like a map to me. Try to parse it as Map<String, List<String>> . Then you can access keys and values separately.

Gson's default DTO fields annotations are used for simple cases. For more complicated deserialization you might want to use custom type adapters and (de)serializers in order to avoid weak typed DTOs like maps and lists in a more Gson-idiomatic way.

Suppose you have the following DTO:

final class Country {

    private final String name;
    private final List<String> cities;

    Country(final String name, final List<String> cities) {
        this.name = name;
        this.cities = cities;
    }

    String getName() {
        return name;
    }

    List<String> getCities() {
        return cities;
    }

}

Assuming a "non-standard" JSON layout, the following deserializer will traverse the JSON object tree recursively in order to collect the target list of countries. Say,

final class CountriesJsonDeserializer
        implements JsonDeserializer<List<Country>> {

    private static final JsonDeserializer<List<Country>> countryArrayJsonDeserializer = new CountriesJsonDeserializer();

    private static final Type listOfStringType = new TypeToken<List<String>>() {
    }.getType();

    private CountriesJsonDeserializer() {
    }

    static JsonDeserializer<List<Country>> getCountryArrayJsonDeserializer() {
        return countryArrayJsonDeserializer;
    }

    @Override
    public List<Country> deserialize(final JsonElement json, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        final List<Country> countries = new ArrayList<>();
        final JsonObject root = json.getAsJsonObject();
        for ( final Entry<String, JsonElement> e : root.entrySet() ) {
            final String name = e.getKey();
            final List<String> cities = context.deserialize(e.getValue(), listOfStringType);
            countries.add(new Country(name, cities));
        }
        return countries;
    }

}

The deserializer above would be bound to a List<Country> mapping. As the very first step it "casts" an abstract JsonElement to a JsonObject in order to traverse over its properties ( "USA" , "Turkey" , and "Lebanon" ). Assuming that the property name is a country name itself, the list of city names (the property value effectively) could be delegated to the serialization context deeper and parsed as a List<String> instance (note the type token). Once both name and cities are parsed, you can construct a Country instance and collect the result list.

How it's used:

private static final Type listOfCountryType = new TypeToken<List<Country>>() {
}.getType();

private static final Gson gson = new GsonBuilder()
        .registerTypeAdapter(listOfCountryType, getCountryArrayJsonDeserializer())
        .create();

public static void main(final String... args) {
    final List<Country> countries = gson.fromJson(JSON, listOfCountryType);
    for ( final Country country : countries ) {
        out.print(country.getName());
        out.print(" => ");
        out.println(country.getCities());
    }
}

Type tokens and Gson instances are known to be thread-safe so they can be stored as final static instances safely. Note the way how the custom type of List<Country> and the custom deserializer of CountriesJsonDeserializer are bound to each other. Once the deserialization is finised, it will output:

USA => [New York, Texas, Hawaii]
Turkey => [Ankara, Istanbul]
Lebanon => [Beirut, Zahle]


Update

Since I have never worked with Retrofit, I tried the following code with this configuration:

  • com.google.code.gson:gson:2.8.0
  • com.squareup.retrofit2:retrofit:2.1.0
  • com.squareup.retrofit2:converter-gson:2.1.0

Define the "geo" service interface:

interface IGeoService {

    @GET("/countries")
    Call<List<Country>> getCountries();

}

And build a Retrofit instance with the custom Gson-aware converter:

// The statics are just borrowed from the example above

public static void main(final String... args) {
    // Build the Retrofit instance
    final Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(... your URL goes here...)
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build();
    // Proxify the geo service by Retrofit
    final IGeoService geoService = retrofit.create(IGeoService.class);
    // Make a call to the remote service
    final Call<List<Country>> countriesCall = geoService.getCountries();
    countriesCall.enqueue(new Callback<List<Country>>() {
        @Override
        public void onResponse(final Call<List<Country>> call, final Response<List<Country>> response) {
            dumpCountries("From a remote JSON:", response.body());
        }

        @Override
        public void onFailure(final Call<List<Country>> call, final Throwable throwable) {
            throw new RuntimeException(throwable);
        }
    });
    // Or just take a pre-defined string
    dumpCountries("From a ready-to-use JSON:", gson.<List<Country>>fromJson(JSON, listOfCountryType));
}

private static void dumpCountries(final String name, final Iterable<Country> countries) {
    out.println(name);
    for ( final Country country : countries ) {
        out.print(country.getName());
        out.print(" => ");
        out.println(country.getCities());
    }
    out.println();
}

In case you have type clashing because of the Country class along with its JSON deserializer (I mean, you already have another "country" class for a different purpose), just rename this Country class in order not to affect "well-working" mappings.

The output:

From a ready-to-use JSON:
USA => [New York, Texas, Hawaii]
Turkey => [Ankara, Istanbul]
Lebanon => [Beirut, Zahle]

From a remote JSON:
USA => [New York, Texas, Hawaii]
Turkey => [Ankara, Istanbul]
Lebanon => [Beirut, Zahle]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM