Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exception at value deserializer (custom deserializer) #853

Closed
shashigomaram opened this issue Jun 30, 2015 · 4 comments
Closed

Exception at value deserializer (custom deserializer) #853

shashigomaram opened this issue Jun 30, 2015 · 4 comments

Comments

@shashigomaram
Copy link

Hi,
I have created a custom JsonDeserializer for one of the type (say, ConnParams), which I'm using in my project. This ConnParams type is an inner class of another type (say, Connection).

In one of the Json response / data, I get an array of connections which will optionally contain connParams data. For the deserialization of connection object, I'm able to deseriailze with the objectMapper. But for deserialization of connParams object, I'm using a custom deserializer which extends JsonDeserializer<Connection.ConnParams>.

It is working fine, when I have a response with array of connections, which DOES NOT contain connParams data for LAST OBJECT.

The problem occurs only when I have connection object with connParams data for the LAST OBJECT.

Works fine for below sample response:

[
  {
    "@type": "connection",
    "id": "0000150B000000000002",
    "port": 0,
    "timeout": 60,
    "connParams": {
      "orgId": "000015",
      "agentId": "00001508000000000002"
    }
  },
  {
    "@type": "connection",
    "id": "0000150B000000000003",
    "port": 0,
    "timeout": 60
  }
]

Fails for below sample response:

[
  {
    "@type": "connection",
    "id": "0000150B000000000002",
    "port": 0,
    "timeout": 60
  },
  {
    "@type": "connection",
    "id": "0000150B000000000003",
    "port": 0,
    "timeout": 60,
    "connParams": {
      "orgId": "000015",
      "agentId": "00001508000000000002"
    }
  }
]

Exception/StackTrace:

com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (null), expected FIELD_NAME: missing property '@type' that is to contain type id  (for class com.test.Connection)
 at [Source: java.io.StringReader@24762cda; line: 1, column: 8234] (through reference chain: Object[][14])
    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:148)
    at com.fasterxml.jackson.databind.DeserializationContext.wrongTokenException(DeserializationContext.java:927)
    at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedUsingDefaultImpl(AsPropertyTypeDeserializer.java:151)
    at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:86)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:957)
    at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:158)
    at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:17)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3560)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2623)

Then, I've downloaded the source code for jackson-databind-version2.5.0 for debugging purpose. While debugging, I found a note/comment like "Note: must handle null explicitly here; value deserializers won't" in class "com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.java" at line 150.

In this class method deserialize(jp, ctxt), at line number 158, I've added a null check on t (a JsonTocken) which would break the while loop.

Can we have something like a flag, which would decide whether to have this null check for deserializers or not ?

Thanks,
Shashi

@cowtowncoder
Copy link
Member

Before figuring out what the proper fix is, it'd be necessary to see the code for POJOs, as well as code you use for deserialization. While exception is thrown from a specific place does not necessarily mean the problem occurred there, since deserializers called earlier may have read too little or too much, leading to insonsistent state.

@shashigomaram
Copy link
Author

Hi, Sorry for the delayed response.

Here is the my service method:

public Object CallInfaService(Object requestObject, Class<?> responseClass) throws Exception {
    Writer  writer                  =   new StringWriter();
    ObjectMapper    objMapper       =   new ObjectMapper();
    objMapper.setSerializationInclusion(Include.NON_NULL);
    objMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    objMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    objMapper.configure(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS, false);
    objMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    SimpleModule simpleModule       =   new SimpleModule();
    simpleModule.addSerializer(new CustomJsonSerializerForConnParamsEntry());
    simpleModule.addDeserializer(Connection.ConnParams.class, new CustomJsonDeserializerForConnParamsEntry());
    objMapper.registerModule(simpleModule);

    String  sResponse   =   null;
    if(requestObject==null){
        sResponse       =   makeHttpRequest(null);
    }else{
        objMapper.writeValue(writer, requestObject);
        sResponse       =   makeHttpRequest(initLowercase(writer.toString()));
    }
    Reader reader           =   new StringReader(sResponse);
    Object reponseObject    =   null;
    try{
        if(getHttpReponse() == 200){
            if(sResponse == null || sResponse.isEmpty() || responseClass == null)
                sResponse   =   null;
            else{
                if (responseClass != null && (responseClass.getSimpleName().startsWith("Connection")) ) {
                    reader          =   new StringReader(initUppercase(sResponse));
                }
                reponseObject   =   objMapper.readValue(reader, responseClass);
            }
        }else
            reponseObject   =   objMapper.readValue(reader, Error.class);
    }catch(Exception e){
        e.printStackTrace();
    }
    return reponseObject;
}

I have a custom deserializer for "connParams", because the fields in "connParams" are not fixed. They will be changing for each response.

Here is my custom deserializer


public class CustomJsonDeserializerForConnParamsEntry extends JsonDeserializer<Connection.ConnParams> {
    @Override
    public Connection.ConnParams deserialize(JsonParser paramJsonParser,
            DeserializationContext paramDeserializationContext)
            throws IOException, JsonProcessingException {
        Connection.ConnParams connParams = new Connection.ConnParams();
        List<Connection.ConnParams.Entry> lstEntries    =   connParams.getEntry();//new ArrayList<Connection.ConnParams.Entry>();

        if(paramJsonParser.getCurrentToken().equals(JsonToken.START_OBJECT)){
            while(!paramJsonParser.isClosed() && !paramJsonParser.getCurrentToken().equals(JsonToken.END_OBJECT)){
                String sKey     =   paramJsonParser.nextFieldName();
                String sValue   =   paramJsonParser.nextTextValue();
                if(sKey != null && !sKey.isEmpty()){
                    Connection.ConnParams.Entry entry   =   new Connection.ConnParams.Entry();
                    entry.setKey(sKey);
                    entry.setValue(sValue);
                    lstEntries.add(entry);
                }
            }
        }
        return connParams;
    }
}

@cowtowncoder
Copy link
Member

Looking at your deserializer, I highly suspect it gets confused with the input and this is the reason for your problems.

The reason is that you are not used nextFieldName() and nextTextValue() in safe way, and the loop will miss matching END_OBJECT, probably ending with end of input instead.

Both methods will return null if the type does not match what you expect (not a FIELD_NAME in first case, or VALUE_STRING for second). Parser will advance to the next token, however, and you can check that it matches.

It is oftentimes more reliably to use nextToken() method, although it is possible to fix this deserializer to ensure that nextFieldName() does NOT return null; or if it does, that this is because current token is now END_OBJECT. Or simply terminate the loop with null.

@shashigomaram
Copy link
Author

Thanks for the info!
Let me try to fix my deserializer, will update you soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants