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

Unable to deserialize to a List[SomeClass] #107

Closed
johnlcox opened this issue Nov 8, 2013 · 10 comments
Closed

Unable to deserialize to a List[SomeClass] #107

johnlcox opened this issue Nov 8, 2013 · 10 comments

Comments

@johnlcox
Copy link

johnlcox commented Nov 8, 2013

I was having trouble deserializing a scala List of a case class in a project. I wrote a couple simple tests below that reproduce the issue with slightly different errors.

case class InListCaseClass(@JsonProperty("id") id: Int)
val list = List(InListCaseClass(2), InListCaseClass(5))
val json = """"[{"id":2},{"id":5}]""""

// JsonMappingException: Can not deserialize instance of scala.collection.immutable.List out of VALUE_STRING token at [Source: java.io.StringReader@12b4333; line: 1, column: 1]
it should "deserialize a List of a case class" in {
  val result: List[InListCaseClass] = deserialize[List[InListCaseClass]](json)

  result should equal(list)
}

// JsonMappingException: Can not construct instance of scala.collection.immutable.List, problem: abstract types either need to be mapped to concrete types, have custom deserializer, or be instantiated with additional type information at [Source: java.io.StringReader@1beb518; line: 1, column: 1]
it should "deserialize a List of a case class using JavaType" in {
  val paramTypes = Array(mapper.getTypeFactory().constructSimpleType(classOf[InListCaseClass],
      Array.empty[JavaType]))
  val listType = mapper.getTypeFactory.constructSimpleType(classOf[List[InListCaseClass]], paramTypes)

  val result: List[InListCaseClass] = mapper.readValue(json, listType)

  result should equal(list)
}
@christophercurrie
Copy link
Member

Your "json" string is overquoted. Scala raw strings have three leading quote marks, not four. Your string is thus being seen by Jackson as a JSON String rather than a JSON Array.

@johnlcox
Copy link
Author

johnlcox commented Nov 8, 2013

Whoops, you are right. I fixed my test cases and it looks like the test case that uses deserialize[T] works but the test case that uses readValue(String, JavaType) is still failing, which matches the case I'm seeing in the project I'm using Jackson in.

case class InListCaseClass(@JsonProperty("id") id: Int)
val list = List(InListCaseClass(2), InListCaseClass(5))
val json = """[{"id":2},{"id":5}]"""

// JsonMappingException: Can not construct instance of scala.collection.immutable.List, problem: abstract types either need to be mapped to concrete types, have custom deserializer, or be instantiated with additional type information at [Source: java.io.StringReader@1beb518; line: 1, column: 1]
it should "deserialize a List of a case class using JavaType" in {
  val paramTypes = Array(mapper.getTypeFactory().constructSimpleType(classOf[InListCaseClass],
      Array.empty[JavaType]))
  val listType = mapper.getTypeFactory.constructSimpleType(classOf[List[InListCaseClass]], paramTypes)

  val result: List[InListCaseClass] = mapper.readValue(json, listType)

  result should equal(list)
}

@christophercurrie
Copy link
Member

Fair enough. TypeFactory, as far as I know, is not intended part of the "public" API of Jackson, but I'm going to confirm with the maintainers on that. The recommended way to achieve what you're trying to do is to use TypeReference instances:

// EDITED to add missing body to anonymous subclass
val listType = new TypeReference[List[InListCaseClass]] {}
val result: List[InListCaseClass] = mapper.readValue(json, listType)

@johnlcox
Copy link
Author

johnlcox commented Nov 8, 2013

In my project I'm working with a library called retrofit for making REST calls and it wraps the Jackson usage via a converter. The class I'm seeing the error in is https://github.com/square/retrofit/blob/master/retrofit-converters/jackson/src/main/java/retrofit/converter/JacksonConverter.java where the JavaType object is similar to my example above (List[SomeClass]).

@christophercurrie
Copy link
Member

The only way that what retrofit is doing will work is if the type argument to fromBody is an instance of ParameterizedType. You can use TypeReference to help you construct one:

val listType = new TypeReference[List[InListCaseClass]] {}
jacksonConverter.fromBody(body, listType.getType())

@johnlcox
Copy link
Author

johnlcox commented Nov 8, 2013

It looks like Retrofit is using reflectcion via MethodInfo#getGenericReturnType which returns a ParamterizedType when the method in question returns a generic type, so it looks like it should be OK there.

@christophercurrie
Copy link
Member

OK. I just got confirmation that TypeFactory is definitely part of the supported public API, so the code is OK there, though the recommendation is to use objectMapper.getTypeFactory rather than TypeFactory.defaultInstance.

Your own test case needs to either look more similar to Retrofit and use a ParameterizedType instead of classOf, or instead use constructCollectionLikeType instead of constructSimpleType, to get the correct JavaType to make it work.

@johnlcox
Copy link
Author

johnlcox commented Nov 8, 2013

Thanks for all the info. Based on everything you have explained I have been able to modify my tests and confirm that this appears to be a retrofit bug due to using TypeFactory.defaultInstance instead of objectMapper.getTypeFactory. I should be able to pull request a fix to Retrofit.

@christophercurrie
Copy link
Member

That seems to be the right approach. The message I got from Jackson is "defaultInstance is really.... like a hack for unit tests ... it's static singleton, so can't register Scala (et al) stuff."

Good luck, and let the Retrofit team know they can ask questions on the Jackson mailing list if they want more explanation.

@cowtowncoder
Copy link
Member

So far so good. The only additional comment for one of code snippets from above is that method constructSimpleType() should also not be used unless one really knows it is a simple type -- generic types, Collection, array and Map types are NOT simple types. Simple means basically non-container, non-generic types.

Instead, constructType() should be used when getting an arbitrary java.lang.reflect.Type, to let TypeFactory introspect things fully.

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

3 participants