How to parse json object with gson based on dynamic SerializedName in Android

前端 未结 3 978
耶瑟儿~
耶瑟儿~ 2021-01-22 05:22

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

3条回答
  •  暗喜
    暗喜 (楼主)
    2021-01-22 05:54

    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:

    • No arrays but artificially indexed object keys (and that's the subject of your question).
    • Empty strings for probably null values rather than nulls or just exclusion from the response.
    • Almost all values are expressed as string literals even if they seem to be non-strings.
    • Probably boolean values seem to be marked with the 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 xxxhour-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='성공'}}

提交回复
热议问题