FasterXML / jackson-databind

General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)
Apache License 2.0
3.52k stars 1.38k forks source link

ObjectMapper.readValue ignores the type #4766

Closed MrMaxxan closed 1 week ago

MrMaxxan commented 1 week ago

Search before asking

Describe the bug

I have java models automatically generated from an asyinc api spec. They look like:

class Header {
String type;
...
}

@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Create.class, name = "Create"),
  @JsonSubTypes.Type(value = Update.class, name = "Update")
})
interface Command {}

class Create implements Command {
int value;
}

class Update implements Command {
int value;
}

I get an object containg the header and the command like {"header": {"type": "Update", ...}, "data": {...}}. Then I read from the header what type of object that should be deserialized, for example

var objectMapper = new ObjectMapper()
  .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
  .registerModule(new JavaTimeModule())
  .registerModule(new Jdk8Module());
JsonNode rootNode = objectMapper.readTree(msg);
objectMapper.readValue(rootNode.get("data"), Update.class);

but readValue ignores that I tell it to create an object of the type Update, it tries to deduct the object and since it can't due to identical classes, it fails to deserialize with the error Could not resolve subtype of [simple type, class Update]: Cannot deduce unique subtype of 'Update' (2 candidates match).

If I already specified which class it is, then it feels like a bug that it tries to deduct the type. Or is this not how the valueType parameter should be used?

Version Information

No response

Reproduction

See above

Expected behavior

I expect it to deserialize to the object of the type I've specified.

Additional context

No response

adrian-enspired commented 1 week ago

this seems like it might be a similar problem to one i've encountered - given an explicit subtype, jackson still tries to guess at what the type should be. in my case, this happens with JsonTypeInfo.Id.NAME. example: https://gist.github.com/adrian-enspired/077fe2c6d1b9d263b00aead2f45e5aa3

cowtowncoder commented 1 week ago

I suspect this might be bit wrong usage: if no type deduction is needed or possible, just leave out @JsonTypeInfo altogether? It does not sound like it has any benefit if type detection is handled by your own code -- no benefit from asking Jackson to do something that is not needed and -- worse-- causes problems.

FWTW, @JsonTypeInfo is not meant as optional in "hey, if you have type cool; if not, fine". It absolutely expects type info. If this is not the case, it's better not to use it.

Specifically the intent is that the target type should always be the type (interface or class) that is annotated with @JsonTypeInfo. And not a sub-type thereof.

MrMaxxan commented 1 week ago

So how to deserialize the model? If I can't change the async API spec (since it's not mine), I can't change how the models are generated from the spec (using asyncapi CLI), how can I deserialize a Create model?

JooHyukKim commented 1 week ago

@MrMaxxan Try looking into use option of @JsonTypeInfo. You will get several options to choose from and they are all documented.

MrMaxxan commented 1 week ago

Since the models are generated (including JsonTypeInfo) I can't really change them. Manual changes would be overwritten next time I build the project since it's generated each time.

Is there a workaround that doesn't include updating the models?

pjfanning commented 1 week ago

@MrMaxxan Use a search engine and look up 'jackson mixins'

cowtowncoder commented 1 week ago

It sounds like generation produces invalid annotations: I don't think @JsonTypeInfo should be added at all if you manually resolve type.

So fixing generator seems like thing to do?

MrMaxxan commented 1 week ago

I will try to see if I can find some workaround. Maybe I need to add a custom deserializer or something.

MrMaxxan commented 1 week ago

If anyone else has this problem, then the solution was to add this to the object mapper: objectMapper.configure(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES, false)