0

I struggle using Spring-boot for creating a REST client for the Synology FileStation API. Indeed, the API uses the same attribute to store different kind of objects. The attribute used is data and it can store either the sid or the content of a search result like file shares.

For search result :

{
  "data": {
    "offset": 0,
    "shares": [
      {
        "isdir": true,
        "name": "config",
        "path": "/config"
      },
      ...
    ],
    "total": 19
  },
  "success": true
}

For login result :

{
  "data": {
    "sid": "blablablabla",
  },
  "success": true
}

For bad login error :

{
  "error": {
    "code": 400,
  },
  "success": false
}

I tried to design model for each type of response, but I cannot find a proper way to use Jackson to deserialize it.

To make it simple, the class that is used for receiving the response is Reponse, in the response there is a success field, and two other fields: Data and Error. Depending on the response, the Data to be instanciated is either a LoginResponse or ListShares object.

Here is the code of my classes :

@JsonIgnoreProperties(ignoreUnknown = true)
public class Response {
    @JsonProperty("success")
    private boolean success;
    @JsonProperty(value = "data")
    private Data data;
    @JsonProperty(value = "error")
    private Error error;
    ...
}

@JsonIgnoreProperties(ignoreUnknown = true)
public class Error {
    private int code;
    private Collection<ErrorInfo> errors;

    public Error( int code, Collection<ErrorInfo> errors) {
        super();
        this.code = code;
        this.errors = errors;
    }
    ...
}


@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.WRAPPER_OBJECT)
@JsonSubTypes({
        @JsonSubTypes.Type(value = LoginResponse.class),
        @JsonSubTypes.Type(value = ListShares.class),
})
public abstract class Data {

}

@JsonTypeName("LoginResponse")
public class LoginResponse extends Data {

    @JsonProperty("sid")
    private String sid;

    public LoginResponse() {
        super();
    }
    ...
}


@JsonTypeName("ListShares")
public class ListShares extends Data {

    @JsonProperty("offset")
    private int offset;
    @JsonProperty("total")
    private int total;
    @JsonProperty("shares")
    private Set<Share> shares;

    public ListShares() {
        super();
    }

    public ListShares(int offset, int total, Set<Share> shares) {
        super();
        this.offset = offset;
        this.total = total;
        this.shares = shares;
    }

I get this exception :

Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class com.heavyrage.syno.apis.genericresponses.Response]: missing type id property 'data'
 at [Source: (PushbackInputStream); line: 1, column: 38]
    at com.fasterxml.jackson.databind.exc.InvalidTypeIdException.from(InvalidTypeIdException.java:43) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.DeserializationContext.missingTypeIdException(DeserializationContext.java:1794) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.DeserializationContext.handleMissingTypeId(DeserializationContext.java:1323) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._handleMissingTypeId(TypeDeserializerBase.java:303) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedUsingDefaultImpl(AsPropertyTypeDeserializer.java:166) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:107) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1209) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:68) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4482) ~[jackson-databind-2.11.1.jar:2.11.1]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3487) ~[jackson-databind-2.11.1.jar:2.11.1]
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:273) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
    ... 17 common frames omitted

Does someone know if it is possible to instanciate the Data object correctly (instanciate LoginResponse or ListShare) to populate the "data" property of the Response object ?

Heavyrage
  • 3
  • 5
  • Your API return different type in error and success response. You can use generic object for your data. Also error field always is present and just in exception scenario has value. Use hasA relationship instead polymorphism. – mohammad_1m2 Aug 06 '20 at 19:04
  • Thanks for your answer, that helped me understand the relation between my Response class and the Data it contains. I tried migrating the Data class to an interface and make LoginResponse and ListShare classes implement this interface. Now I end up with a new exception because Jackson tries to instantiate the Data interface, which cannot be done. Is there a way to use Composition with Jackson using Java interfaces? – Heavyrage Aug 08 '20 at 09:36

1 Answers1

0

I finally found this answer from the Jackson issue tracker. The key is the deserializer to be called when instanciating the Data object, thus the correct implementation class is returned and Jackson can instanciate it.

https://stackoverflow.com/a/50013090/1030527

Anyway, it was a good idea to use composition for the the Response class because it reflects its HasA relationship with both Data and Error classes.

For the Data class itself, using composition with Java interface led to another exception caused by Jackson trying to instanciate the interface class.

Heavyrage
  • 3
  • 5