Reputation: 16639
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.
Upvotes: 1
Views: 2260
Reputation: 21145
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]
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]
Upvotes: 1
Reputation: 2266
Looks like a map to me. Try to parse it as Map<String, List<String>>
. Then you can access keys and values separately.
Upvotes: 4