Десериализация 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 шт):

Автор решения: Alexandr

Самый простой способ это использовать 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);
    }
}
→ Ссылка
Автор решения: MShkolniy

Большое спасибо всем откликнувшимся! Очень оперативно!

@Alexandr - спасибо за столь развернутый ответ (первый предложенный вариант сработал).

@AlekseiGaile - спасибо за ссылки!

@tym32167 - спасибо, Ваш вариант был верным, только моя реализация была крайне неудачной и в итоге не работала.. или я не указал нужную аннотацию... Александр предложил рабочий код.

В общем, вот так получилось распарсить мой JSON сразу в мапу, без лишних классов и строк:

public class Rate {

    ...

    @JsonProperty("rates")
    private Map<String, BigDecimal> rates;

    ...

}

p.s. И да, это был мой самый первый вопрос на stackoverflow, поэтому положительные эмоции переполняют))

→ Ссылка