How can I parse this json objects with Gson this url: http://apis.skplanetx.com/weather/forecast/3hours?appKey=4ce0462a-3884-30ab-ab13-93efb1bc171f&version=1&lon=127.925
The current JSON response accessible via the link you provided seems to have a few design issues or suspicions for them. I'll post the JSON here in order not to let it be lost in the future:
{
"weather": {
"forecast3hours": [
{
"grid": {
"city": "충북",
"county": "충주시",
"village": "목행동",
"latitude": "37.0135600000",
"longitude": "127.9036500000"
},
"lightning1hour": "0",
"timeRelease": "2017-02-24 16:30:00",
"wind": {
"wspd2hour": "3.10",
"wdir1hour": "179.00",
"wspd1hour": "4.20",
"wdir2hour": "176.00",
"wdir3hour": "",
"wspd3hour": "",
"wdir4hour": "",
"wspd4hour": ""
},
"precipitation": {
"sinceOntime1hour": "0.00",
"type1hour": "0",
"sinceOntime2hour": "0.00",
"type2hour": "0",
"sinceOntime3hour": "",
"type3hour": "",
"sinceOntime4hour": "",
"type4hour": ""
},
"sky": {
"code1hour": "SKY_V01",
"name1hour": "맑음",
"code2hour": "SKY_V01",
"name2hour": "맑음",
"code3hour": "",
"name3hour": "",
"code4hour": "",
"name4hour": ""
},
"temperature": {
"temp1hour": "3.20",
"temp2hour": "2.00",
"temp3hour": "",
"temp4hour": ""
},
"humidity": {
"rh1hour": "41.00",
"rh2hour": "50.00",
"rh3hour": "",
"rh4hour": ""
},
"lightning2hour": "0",
"lightning3hour": "",
"lightning4hour": ""
}
]
},
"common": {
"alertYn": "N",
"stormYn": "N"
},
"result": {
"code": 9200,
"requestUrl": "/weather/forecast/3hours?lon=127.9259&lat=36.991&version=1&appKey=4ce0462a-3884-30ab-ab13-93efb1bc171f",
"message": "성공"
}
}
From my point of view they are:
null
values rather than null
s or just exclusion from the response.Yn
suffix, and define true
and false
using "Y"
and "N"
respectively.This is why automatic POJO generators may be not the best way to deal with it because they may do not detect "real" type of a particular JSON string value, and moreover they cannot generate custom deserializers. Not sure why it was designed that way, but you can design your custom mappings to make it a little more programmatically-friendly with more control over them.
final class Response {
final Weather weather = null;
final Common common = null;
final Result result = null;
@Override
public String toString() {
return new StringBuilder("Response{")
.append("weather=").append(weather)
.append(", common=").append(common)
.append(", result=").append(result)
.append('}').toString();
}
}
final class Weather {
final List forecast3hours = null;
@Override
public String toString() {
return new StringBuilder("Weather{")
.append("forecast3hours=").append(forecast3hours)
.append('}').toString();
}
}
final class Forecast {
final Grid grid;
final Date timeRelease;
final List lightnings;
final List winds;
final List precipitations;
final List skies;
final List temperatures;
final List humidities;
Forecast(final Grid grid, final Date timeRelease, final List lightnings, final List winds, final List precipitations,
final List skies, final List temperatures, final List humidities) {
this.grid = grid;
this.timeRelease = timeRelease;
this.lightnings = lightnings;
this.winds = winds;
this.precipitations = precipitations;
this.skies = skies;
this.temperatures = temperatures;
this.humidities = humidities;
}
@Override
public String toString() {
return new StringBuilder("Forecast{")
.append("grid=").append(grid)
.append(", timeRelease=").append(timeRelease)
.append(", lightnings=").append(lightnings)
.append(", winds=").append(winds)
.append(", precipitations=").append(precipitations)
.append(", skies=").append(skies)
.append(", temperatures=").append(temperatures)
.append(", humidities=").append(humidities)
.append('}').toString();
}
}
final class Grid {
final String city = null;
final String county = null;
final String village = null;
final double latitude = Double.valueOf(0); // disable inlining the primitive double 0
final double longitude = Double.valueOf(0); // disable inlining the primitive double 0
@Override
public String toString() {
return new StringBuilder("Grid{")
.append("city='").append(city).append('\'')
.append(", county='").append(county).append('\'')
.append(", village='").append(village).append('\'')
.append(", latitude=").append(latitude)
.append(", longitude=").append(longitude)
.append('}').toString();
}
}
final class Wind {
final float speed;
final float direction;
Wind(final float speed, final float direction) {
this.speed = speed;
this.direction = direction;
}
@Override
public String toString() {
return new StringBuilder("Wind{")
.append("speed=").append(speed)
.append(", direction=").append(direction)
.append('}').toString();
}
}
final class Precipitation {
final float sinceOntime;
final int type;
Precipitation(final float sinceOntime, final int type) {
this.sinceOntime = sinceOntime;
this.type = type;
}
@Override
public String toString() {
return new StringBuilder("Precipitation{")
.append("sinceOntime='").append(sinceOntime).append('\'')
.append(", type=").append(type)
.append('}').toString();
}
}
final class Sky {
final String code;
final String name;
Sky(final String code, final String name) {
this.code = code;
this.name = name;
}
@Override
public String toString() {
return new StringBuilder("Sky{")
.append("code='").append(code).append('\'')
.append(", name='").append(name).append('\'')
.append('}').toString();
}
}
final class Common {
@SerializedName("alertYn")
@JsonAdapter(YnToBooleanJsonDeserializer.class)
final boolean alert = Boolean.valueOf(false); // disable inlining the primitive boolean false
@SerializedName("stormYn")
@JsonAdapter(YnToBooleanJsonDeserializer.class)
final boolean storm = Boolean.valueOf(false); // disable inlining the primitive boolean false
@Override
public String toString() {
return new StringBuilder("Common{")
.append("alert=").append(alert)
.append(", storm=").append(storm)
.append('}').toString();
}
}
final class Result {
final int code = Integer.valueOf(0); // disable inlining the primitive int 0
final String requestUrl = null;
final String message = null;
@Override
public String toString() {
return new StringBuilder("Result{")
.append("code=").append(code)
.append(", requestUrl='").append(requestUrl).append('\'')
.append(", message='").append(message).append('\'')
.append('}').toString();
}
}
Some of these mappings have explicit constructors - such objects must be instantiated manually in custom deserializers. If there is no constructors provided, then Gson can deal with such mapping itself having just enough information on how a particular object should be deserialized.
Since that data should be parsed in a non-standard way, a couple of custom deserializators can be implemented. The following type adapter converts "Y"
and "N"
to true
and false
respectively.
final class YnToBooleanJsonDeserializer
implements JsonDeserializer {
// Gson will instantiate this adapter itself
private YnToBooleanJsonDeserializer() {
}
@Override
public Boolean deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
final String rawFlag = jsonElement.getAsString();
switch ( rawFlag ) {
case "N":
return false;
case "Y":
return true;
default:
throw new JsonParseException("Can't parse: " + rawFlag);
}
}
}
The next JsonDeserializer
tries to detect xxx
-like keys with regular expressions and extracting the
index building the lists required to construct a Forecast
instance. Note that it can parse "lists" (the ones in the JSON) of arbitrary size.
final class ForecastJsonDeserializer
implements JsonDeserializer {
// This deserializer does not hold any state and can be instantiated once per application life-cycle.
private static final JsonDeserializer forecastJsonDeserializer = new ForecastJsonDeserializer();
private ForecastJsonDeserializer() {
}
static JsonDeserializer getForecastJsonDeserializer() {
return forecastJsonDeserializer;
}
@Override
public Forecast deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
final JsonObject jsonObject = jsonElement.getAsJsonObject();
return new Forecast(
context.deserialize(jsonObject.get("grid"), Grid.class),
context.deserialize(jsonObject.get("timeRelease"), Date.class),
lightningsExtractor.parseList(jsonObject),
windsExtractor.parseList(jsonObject.get("wind").getAsJsonObject()),
precipitationsExtractor.parseList(jsonObject.get("precipitation").getAsJsonObject()),
skiesExtractor.parseList(jsonObject.get("sky").getAsJsonObject()),
temperaturesExtractor.parseList(jsonObject.get("temperature").getAsJsonObject()),
humiditiesExtractor.parseList(jsonObject.get("humidity").getAsJsonObject())
);
}
private static final AbstractExtractor lightningsExtractor = new AbstractExtractor(compile("lightning(\\d)hour")) {
@Override
protected Integer parse(final int index, final JsonObject jsonObject) {
final String rawLightning = jsonObject.get("lightning" + index + "hour").getAsString();
if ( rawLightning.isEmpty() ) {
return null;
}
return parseInt(rawLightning);
}
};
private static final AbstractExtractor windsExtractor = new AbstractExtractor(compile("(?:wdir|wspd)(\\d)hour")) {
@Override
protected Wind parse(final int index, final JsonObject jsonObject) {
String rawSpeed = jsonObject.get("wspd" + index + "hour").getAsString();
String rawDirection = jsonObject.get("wdir" + index + "hour").getAsString();
if ( rawSpeed.isEmpty() && rawDirection.isEmpty() ) {
return null;
}
return new Wind(parseFloat(rawSpeed), parseFloat(rawDirection));
}
};
private static final AbstractExtractor precipitationsExtractor = new AbstractExtractor(compile("(?:sinceOntime|type)(\\d)hour")) {
@Override
protected Precipitation parse(final int index, final JsonObject jsonObject) {
final String rawSinceOntime = jsonObject.get("sinceOntime" + index + "hour").getAsString();
final String rawType = jsonObject.get("type" + index + "hour").getAsString();
if ( rawSinceOntime.isEmpty() && rawType.isEmpty() ) {
return null;
}
return new Precipitation(parseFloat(rawSinceOntime), parseInt(rawType));
}
};
private static final AbstractExtractor skiesExtractor = new AbstractExtractor(compile("(?:code|name)(\\d)hour")) {
@Override
protected Sky parse(final int index, final JsonObject jsonObject) {
final String rawCode = jsonObject.get("code" + index + "hour").getAsString();
final String rawName = jsonObject.get("name" + index + "hour").getAsString();
if ( rawCode.isEmpty() && rawName.isEmpty() ) {
return null;
}
return new Sky(rawCode, rawName);
}
};
private static final AbstractExtractor temperaturesExtractor = new AbstractExtractor(compile("temp(\\d)hour")) {
@Override
protected Float parse(final int index, final JsonObject jsonObject) {
final String rawTemperature = jsonObject.get("temp" + index + "hour").getAsString();
if ( rawTemperature.isEmpty() ) {
return null;
}
return parseFloat(rawTemperature);
}
};
private static final AbstractExtractor humiditiesExtractor = new AbstractExtractor(compile("rh(\\d)hour")) {
@Override
protected Float parse(final int index, final JsonObject jsonObject) {
final String rawHumidity = jsonObject.get("rh" + index + "hour").getAsString();
if ( rawHumidity.isEmpty() ) {
return null;
}
return parseFloat(rawHumidity);
}
};
private abstract static class AbstractExtractor {
private final Pattern pattern;
private AbstractExtractor(final Pattern pattern) {
this.pattern = pattern;
}
protected abstract T parse(int index, JsonObject jsonObject);
private List parseList(final JsonObject jsonObject) {
final List list = new ArrayList<>();
for ( final Entry e : jsonObject.entrySet() ) {
final String key = e.getKey();
final Matcher matcher = pattern.matcher(key);
// Check if the given regular expression matches the key
if ( matcher.matches() ) {
// If yes, then just extract and parse the index
final int index = parseInt(matcher.group(1));
// And check if there is enough room in the result list because the JSON response may contain unordered keys
while ( index > list.size() ) {
list.add(null);
}
// As Java lists are 0-based
if ( list.get(index - 1) == null ) {
// Assuming that null marks an object that's probably not parsed yet
list.set(index - 1, parse(index, jsonObject));
}
}
}
return list;
}
}
}
And now it all can be put together:
public static void main(final String... args) {
final Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd hh:mm:ss")
.registerTypeAdapter(Forecast.class, getForecastJsonDeserializer())
.create();
final Response response = gson.fromJson(JSON, Response.class);
System.out.println(response);
}
The output:
Response{weather=Weather{forecast3hours=[Forecast{grid=Grid{city='충북', county='충주시', village='목행동', latitude=37.01356, longitude=127.90365}, timeRelease=Fri Feb 24 16:30:00 EET 2017, lightnings=[0, 0, null, null], winds=[Wind{speed=4.2, direction=179.0}, Wind{speed=3.1, direction=176.0}, null, null], precipitations=[Precipitation{sinceOntime='0.0', type=0}, Precipitation{sinceOntime='0.0', type=0}, null, null], skies=[Sky{code='SKY_V01', name='맑음'}, Sky{code='SKY_V01', name='맑음'}, null, null], temperatures=[3.2, 2.0, null, null], humidities=[41.0, 50.0, null, null]}]}, common=Common{alert=false, storm=false}, result=Result{code=9200, requestUrl='/weather/forecast/3hours?lon=127.9259&lat=36.991&version=1&appKey=4ce0462a-3884-30ab-ab13-93efb1bc171f', message='성공'}}