Десериализация JSON. Java
Всем привет! Я недавно изучаю Java, прошу сильно не ругать за, возможно, глупый вопрос. Передо мной стоит задача - получить текущий курс валют от удаленного сервера API(дальше мне потребуется проделать еще несколько действий с полученным результатом, но суть данного топика в другом). Ответ от сервера приходит в следующем формате:
{
disclaimer: "https://openexchangerates.org/terms/",
license: "https://openexchangerates.org/license/",
timestamp: 1449877801,
base: "USD",
rates: {
AED: 3.672538,
AFN: 66.809999,
ALL: 125.716501,
AMD: 484.902502,
ANG: 1.788575,
AOA: 135.295998,
ARS: 9.750101,
AUD: 1.390866,
/* ...*/
}
}
Количество валют, как вы понимаете, большое (более ста наименований). Само приложение должно быть написано на Spring Boot 2, сборщик Gradle. Для взаимодействия с внешними сервисами используется Feign. Я не понимаю как должны выглядеть мои классы (модели), чтобы распарсить такой JSON. На данный момент код выглядит следующим образом:
Сервис:
@FeignClient(name = "Rate", url = "https://openexchangerates.org/api")
public interface RateService {
@GetMapping("/latest.json")
RateMainJSON getAllLatestRates(@RequestParam String app_id);
}
Контроллер:
@RestController
@EnableAutoConfiguration
@RequestMapping("/")
public class RateController {
private static final String APP_ID = "1ce73ad6a57744ba8514dd76d877b297";
@Autowired
private RateService rateService;
@GetMapping("latest.json")
RateMainJSON getAllLatestRates(@RequestParam String app_id) {
RateMainJSON rates = rateService.getAllLatestRates(APP_ID);
return rates;
}
}
Класс(модель), который включает в себя все строки общей структуры принимаемого JSON:
public class RateMainJSON {
@JsonProperty("disclaimer")
private String disclaimer;
@JsonProperty("license")
private String license;
@JsonProperty("timestamp")
private Long timestamp;
@JsonProperty("base")
private String base;
@JsonProperty("rates")
private Rate rates;
}
и класс(модель), который в моей реализации способен распознать только один вид валюты "RUB":
public class Rate {
@JsonProperty("RUB")
private BigDecimal RUB;
public Rate() {
}
Также пробовал в этом классе создавать только одно поле Map<String, BigDecimal>, но получал просто null вместо списка валют.
В результате всего своего кода я получаю следующий вывод:
{
"disclaimer": "Usage subject to terms: https://openexchangerates.org/terms",
"license":"https://openexchangerates.org/license",
"timestamp":1614009600,
"base":"USD",
"rates": {
"RUB":74.384
}
}
Но, как вы понимаете, мне нужен весь список валют. Я конечно могу перечислить в классе Rate все существующие валюты, но тогда в нём будет более 100 полей, плюс, если на сайте будет добавлена или убрана какая-нибудь валюта, то моё приложение на это не среагирует. Должен быть какой-то лаконичный способ. Также, я хотел бы избавиться от класса RateMainJSON, так как мне вообще не нужны строчки disclaimer, license, timestamp, base, rates.
Кажется, что вопрос элементарный(распарсить JSON), но я уже пару дней штрудирую разные ресурсы и не могу найти способ распарсить.
Заранее спасибо, если кто-то направит ход моих мыслей в нужное русло.
Ответы (2 шт):
Самый простой способ это использовать java.util.Map:
public class RateMainDto {
...
@JsonProperty("rates")
private Map<String, Double> rates;
...
}
Однако если хочется чтобы маппинг происходил в наш класс, то Вам будет необходимо написать свой com.fasterxml.jackson.databind.JsonDeserializer.
RatesDeserializer:
public class RatesDeserializer
extends JsonDeserializer<List<RateDto>> {
@Override
public List<RateDto> deserialize(
JsonParser p,
DeserializationContext ctxt) throws IOException {
List<RateDto> rates =
new ArrayList<>();
while (true) {
String code =
p.nextFieldName();
if (isNull(code)) {
break;
}
JsonToken token =
p.nextValue();
if (token.isNumeric()) {
RateDto rate =
new RateDto();
rate.setCode(code);
rate.setRate(
p.getDoubleValue());
rates.add(rate);
}
}
return rates;
}
}
RateDto:
public class RateDto {
private String code;
private double rate;
...
}
RateMainDto:
public class RateMainDto {
...
@JsonProperty("rates")
@JsonDeserialize(using = RatesDeserializer.class)
private List<RateDto> rates;
...
}
DeserializationTest:
class DeserializationTest {
private static ObjectMapper mapper;
@BeforeAll
static void beforeAll() {
mapper = new ObjectMapper();
}
@Test
public void deserialize() throws JsonProcessingException {
String json =
"{" +
"\"disclaimer\": \"https://openexchangerates.org/terms/\"," +
"\"license\": \"https://openexchangerates.org/license/\"," +
"\"timestamp\": 1449877801," +
"\"base\": \"USD\"," +
"\"rates\": {" +
"\"AED\": 3.672538," +
"\"AFN\": 66.809999," +
"\"ALL\": 125.716501," +
"\"AMD\": 484.902502," +
"\"ANG\": 1.788575," +
"\"AOA\": 135.295998," +
"\"ARS\": 9.750101," +
"\"AUD\": 1.390866" +
"}" +
"}";
RateMainDto rateMain =
mapper.readValue(
json,
RateMainDto.class);
assertNotNull(rateMain);
assertNotNull(rateMain.getRates());
assertEquals(rateMain.getRates().size(), 8);
assertEquals(rateMain.getRates().get(0).getCode(), "AED");
assertEquals(rateMain.getRates().get(0).getRate(), 3.672538);
}
}
Большое спасибо всем откликнувшимся! Очень оперативно!
@Alexandr - спасибо за столь развернутый ответ (первый предложенный вариант сработал).
@AlekseiGaile - спасибо за ссылки!
@tym32167 - спасибо, Ваш вариант был верным, только моя реализация была крайне неудачной и в итоге не работала.. или я не указал нужную аннотацию... Александр предложил рабочий код.
В общем, вот так получилось распарсить мой JSON сразу в мапу, без лишних классов и строк:
public class Rate {
...
@JsonProperty("rates")
private Map<String, BigDecimal> rates;
...
}
p.s. И да, это был мой самый первый вопрос на stackoverflow, поэтому положительные эмоции переполняют))