Deserialize a json array to objects using Jackson and WebClient
Asked Answered
C

3

42

I have a problem during the deserialization of a json array using Spring. I have this json response from a service:

[
    {
        "symbol": "XRPETH",
        "orderId": 12122,
        "clientOrderId": "xxx",
        "price": "0.00000000",
        "origQty": "25.00000000",
        "executedQty": "25.00000000",
        "status": "FILLED",
        "timeInForce": "GTC",
        "type": "MARKET",
        "side": "BUY",
        "stopPrice": "0.00000000",
        "icebergQty": "0.00000000",
        "time": 1514558190255,
        "isWorking": true
    },
    {
        "symbol": "XRPETH",
        "orderId": 1212,
        "clientOrderId": "xxx",
        "price": "0.00280000",
        "origQty": "24.00000000",
        "executedQty": "24.00000000",
        "status": "FILLED",
        "timeInForce": "GTC",
        "type": "LIMIT",
        "side": "SELL",
        "stopPrice": "0.00000000",
        "icebergQty": "0.00000000",
        "time": 1514640491287,
        "isWorking": true
    },
    ....
]

I get this json using the new WebClient from Spring WebFlux, here the code:

@Override
    public Mono<AccountOrderList> getAccountOrders(String symbol) {
        return binanceServerTimeApi.getServerTime().flatMap(serverTime -> {
            String apiEndpoint = "/api/v3/allOrders?";
            String queryParams = "symbol=" +symbol.toUpperCase() + "&timestamp=" + serverTime.getServerTime();
            String signature = HmacSHA256Signer.sign(queryParams, secret);
            String payload = apiEndpoint + queryParams + "&signature="+signature;
            log.info("final endpoint:"+ payload);
            return this.webClient
                    .get()
                    .uri(payload)
                    .accept(MediaType.APPLICATION_JSON)
                    .retrieve()
                    .bodyToMono(AccountOrderList.class)
                    .log();
        });
    }

AccountOrderList

public class AccountOrderList {

    private List<AccountOrder> accountOrders;

    public AccountOrderList() {
    }

    public AccountOrderList(List<AccountOrder> accountOrders) {
        this.accountOrders = accountOrders;
    }

    public List<AccountOrder> getAccountOrders() {
        return accountOrders;
    }

    public void setAccountOrders(List<AccountOrder> accountOrders) {
        this.accountOrders = accountOrders;
    }
}

AccountOrder is a simple pojo that maps the fields.

Actually, when I hit a get it says:

org.springframework.core.codec.DecodingException: JSON decoding error: Cannot deserialize instance of `io.justin.demoreactive.domain.AccountOrder` out of START_ARRAY token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `io.justin.demoreactive.domain.AccountOrder` out of START_ARRAY token
 at [Source: UNKNOWN; line: -1, column: -1]

How can I deserialize the json properly using the new webflux module? What am I doing wrong?

UPDATE 05/02/2018

Both answers are correct. They addressed perfectly my question but at the end I decided to use a slightly different approach:

@Override
    public Mono<List<AccountOrder>> getAccountOrders(String symbol) {
        return binanceServerTimeApi.getServerTime().flatMap(serverTime -> {
            String apiEndpoint = "/api/v3/allOrders?";
            String queryParams = "symbol=" +symbol.toUpperCase() + "&timestamp=" + serverTime.getServerTime();
            String signature = HmacSHA256Signer.sign(queryParams, secret);
            String payload = apiEndpoint + queryParams + "&signature="+signature;
            log.info("final endpoint:"+ payload);
            return this.webClient
                    .get()
                    .uri(payload)
                    .accept(MediaType.APPLICATION_JSON)
                    .retrieve()
                    .bodyToFlux(AccountOrder.class)
                    .collectList()
                    .log();
        });
    }

An alternative to this could be to return directly A Flux so you don't have to convert it to a list. (that's what flux are: a collection of n elements).

Cavite answered 3/2, 2018 at 14:6 Comment(2)
do you create above response or you get this response from 3rd party ?Automate
it's a response from 3rd party. I can't change the response @AutomateCavite
E
27

For the response to be matched with AccountOrderList class, json has to be like this

{
  "accountOrders": [
    {
        "symbol": "XRPETH",
        "orderId": 12122,
        "clientOrderId": "xxx",
        "price": "0.00000000",
        "origQty": "25.00000000",
        "executedQty": "25.00000000",
        "status": "FILLED",
        "timeInForce": "GTC",
        "type": "MARKET",
        "side": "BUY",
        "stopPrice": "0.00000000",
        "icebergQty": "0.00000000",
        "time": 1514558190255,
        "isWorking": true
    },
    {
        "symbol": "XRPETH",
        "orderId": 1212,
        "clientOrderId": "xxx",
        "price": "0.00280000",
        "origQty": "24.00000000",
        "executedQty": "24.00000000",
        "status": "FILLED",
        "timeInForce": "GTC",
        "type": "LIMIT",
        "side": "SELL",
        "stopPrice": "0.00000000",
        "icebergQty": "0.00000000",
        "time": 1514640491287,
        "isWorking": true
    },
    ....
]
}

This is what the error message says "out of START_ARRAY token"

If you cannot change the response, then change your code to accept Array like this

this.webClient.get().uri(payload).accept(MediaType.APPLICATION_JSON)
                        .retrieve().bodyToMono(AccountOrder[].class).log();

You can convert this array to List and then return.

El answered 3/2, 2018 at 14:35 Comment(0)
M
62

Regarding your updated answer to your question, using bodyToFlux is unnecessarily inefficient and semantically doesn't make much sense either as you don't really want a stream of orders. What you want is simply to be able to parse the response as a list.

bodyToMono(List<AccountOrder>.class) won't work due to type erasure. You need to be able to retain the type at runtime, and Spring provides ParameterizedTypeReference for that:

bodyToMono(new ParameterizedTypeReference<List<AccountOrder>>() {})
Margravine answered 2/1, 2019 at 23:2 Comment(4)
Thank you! I was in the same precarious situation where the response from the endpoint was an array of item objects. However, I wanted to ask an extension of this question, Is there a good way to map this response to a response wrapper class? e.g. to something like ` { "itemCount" : 10, "itemsList" : [ { item1 }, { item2 }, ... ] } `Backplate
That's possible but you'd have to use custom deserializers. I personally avoid doing that and prefer to make the transformation explicit by having a mapping function between the data object and the domain one.Margravine
got it, thanks! I made different Response class and used the map step and Builder() to build the new response!Backplate
Thank you .. this helpsHumber
E
27

For the response to be matched with AccountOrderList class, json has to be like this

{
  "accountOrders": [
    {
        "symbol": "XRPETH",
        "orderId": 12122,
        "clientOrderId": "xxx",
        "price": "0.00000000",
        "origQty": "25.00000000",
        "executedQty": "25.00000000",
        "status": "FILLED",
        "timeInForce": "GTC",
        "type": "MARKET",
        "side": "BUY",
        "stopPrice": "0.00000000",
        "icebergQty": "0.00000000",
        "time": 1514558190255,
        "isWorking": true
    },
    {
        "symbol": "XRPETH",
        "orderId": 1212,
        "clientOrderId": "xxx",
        "price": "0.00280000",
        "origQty": "24.00000000",
        "executedQty": "24.00000000",
        "status": "FILLED",
        "timeInForce": "GTC",
        "type": "LIMIT",
        "side": "SELL",
        "stopPrice": "0.00000000",
        "icebergQty": "0.00000000",
        "time": 1514640491287,
        "isWorking": true
    },
    ....
]
}

This is what the error message says "out of START_ARRAY token"

If you cannot change the response, then change your code to accept Array like this

this.webClient.get().uri(payload).accept(MediaType.APPLICATION_JSON)
                        .retrieve().bodyToMono(AccountOrder[].class).log();

You can convert this array to List and then return.

El answered 3/2, 2018 at 14:35 Comment(0)
A
13

Your response is simply List<AccountOrder>. But, your POJO has wrapped List<AccountOrder>. So, according to your POJO, your JSON should be

{
  "accountOrders": [
    {

But, your JSON is

[
    {
       "symbol": "XRPETH",
       "orderId": 12122,
        ....

So, there is mismatch and failing the deserialization. You need to change to

bodyToMono(AccountOrder[].class)
Automate answered 3/2, 2018 at 15:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.