-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Search before asking
- I searched in the issues and found nothing similar.
Describe the bug
Hi, we really like this library, and have been pushing it to it's limits recently. Perhaps I am doing something wrong, but I believe I have found a corner case that isn't quite covered by the type deduction.
I have a sealed class which permits both a parent class and a child class. If the json should deserialize to the child class then things work as expected. If the json should deserialize to the parent class then it cannot decide between the two. The child class contains one extra field in addition to the ones it inherits from the parent.
Error:
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class com.demo.app.example.MyParentClass]: Cannot deduce unique subtype of
com.demo.app.example.MyParentClass(2 candidates match)
at [Source: REDACTED (StreamReadFeature.INCLUDE_SOURCE_IN_LOCATIONdisabled); line: 1, column: 46]
I am using type deduction.
Interestingly, if I remove the parent child relationship from the 2 classes permitted by the sealed class while keeping the json fields the same, then it works fine. Ditto if the parent and child are no longer inside a sealed class. That's what leads me to believe it may be something small missing in the implementation.
Version Information
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.2</version>
</dependency>
Reproduction
Snippet to run the test:
String JsonChild = "{\"myString\":\"my child string\",\"mySize\":1000.0,\"startDate\":\"2025-07-01\"}";
String JsonParent = "{\"myString\":\"my child string\",\"mySize\":1000.0}";
try {
ObjectMapper mapper = new ObjectMapper();
MyChildClass objMyChild = mapper.readerFor(MyChildClass.class).readValue(JsonChild);
MySealedClass obj = mapper.readerFor(MySealedClass.class).readValue(JsonChild);
switch(obj){
case MyChildClass x ->{
log.info("deserialized to child class");
}
case MyParentClass x ->{
log.info("deserialized to parent class");
}
}
// This already fails on the line below
MyParentClass objMyParent= mapper.readerFor(MyParentClass.class).readValue(JsonParent);
MySealedClass obj2 = mapper.readerFor(MySealedClass.class).readValue(JsonParent);
switch(obj2){
case MyChildClass x ->{
log.info("deserialized to child class");
}
case MyParentClass x ->{
log.info("deserialized to parent class");
}
}
} catch (Exception e) {
log.info(String.valueOf(e));
}Sealed Class
package com.demo.app.example;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
@JsonTypeInfo(
use = Id.DEDUCTION
)
@JsonSubTypes({ @Type(MyParentClass.class), @Type(MyChildClass.class)})
public sealed interface MySealedClass permits MyParentClass, MyChildClass {
}Parent Class
package com.demo.app.example;
import com.fasterxml.jackson.annotation.*;
import java.math.BigDecimal;
import java.util.Objects;
import org.springframework.lang.Nullable;
public non-sealed class MyParentClass implements MySealedClass{
public static String tag = "data::example::MyParentClass";
private String myString;
private BigDecimal mySize;
public String getMyString() {
return this.myString;
}
public void setMyString(String myString) {
this.myString = myString;
}
public BigDecimal getMySize() {
return this.mySize;
}
public void setMySize(BigDecimal mySize) {
this.mySize = mySize;
}
public MyParentClass() {
}
public MyParentClass(String myString, @Nullable BigDecimal mySize) {
this.setMyString(myString);
this.setMySize((BigDecimal)Objects.requireNonNullElse(mySize, new BigDecimal((double)1.0F)));
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj != null && obj.getClass() == this.getClass()) {
MyParentClass objMyParent = (MyParentClass)obj;
return this.myString.equals(objMyParent.getMyString()) && this.mySize.equals(objMyParent.getMySize());
} else {
return false;
}
}
public int hashCode() {
return Objects.hash(new Object[]{this.myString, this.mySize});
}
}Child Class
package com.demo.app.example;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Objects;
import org.springframework.lang.Nullable;
public non-sealed class MyChildClass extends MyParentClass implements MySealedClass{
public static String tag = "clearing::data::gen::product::AveragingFuture";
@JsonProperty(required = true)
@JsonFormat(
shape = Shape.STRING,
pattern = "yyyy-MM-dd"
)
private Date startDate;
public Date getStartDate() {
return this.startDate;
}
public void setStartDate(Date startDate) {
this.startDate = startDate;
}
public MyChildClass() {
}
@JsonCreator
public MyChildClass(@JsonProperty("myString")String myString,
@JsonProperty("mySize") @Nullable BigDecimal mySize,
@JsonProperty(value = "startDate", required = true) Date startDate) {
super(myString, mySize);
this.setStartDate((Date)Objects.requireNonNull(startDate));
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj != null && obj.getClass() == this.getClass()) {
MyChildClass objMyChild = (MyChildClass)obj;
return super.equals(obj) && this.startDate.equals(objMyChild.getStartDate());
} else {
return false;
}
}
public int hashCode() {
return Objects.hash(new Object[]{super.hashCode(), this.startDate});
}
}Expected behavior
I added a comment on the line of code that fails, i.e. the place where we are trying to deserialize a message for the parent type
Additional context
Thanks for your time in looking at this!