jackson-databind
jackson-databind copied to clipboard
@JsonDeserialize(contentUsing=...) is ignored if content type is determined by @JsonTypeInfo
Tested versions: 2.9.0.pr3, 2.8.8 and 2.0.1
Expected behavior: Jackson respects @JsonDeserialize(contentUsing=...)
even if the content's type is determined via @JsonTypeInfo
.
Actual behavior: @JsonDeserialize(contentUsing=...)
is ignored.
The following example demonstrates the issue. The expected output is "ok". The observed output is empty. BarDeserializer is never called (nor constructed for that matter).
Removing the @JsonTypeInfo
line "fixes" the example.
public class Test {
static class BarDeserializer extends JsonDeserializer<Bar> {
@Override
public Bar deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
System.out.println("ok");
return p.readValueAs(Bar.class);
}
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
static class Bar {
public int dummy;
}
static class Foo {
@JsonDeserialize(contentUsing = BarDeserializer.class)
public List<Bar> bars;
}
public static void main(String[] args) throws IOException {
Foo foo = new Foo();
foo.bars = Arrays.asList(new Bar());
ObjectMapper om = new ObjectMapper();
om.readValue(om.writeValueAsString(foo), Foo.class);
}
}
Hmmh. I will have to think hard for what would be the expected behavior. I can see why you would expect override to work here, but unfortunately due to handling of content (JsonDeserializer
) being distinct from processing of polymorphic type (TypeDeserializer
) it may be the case that this is actually working the way things need to, in order for all delegation to work as expected.
One thing that may help resolve this locally would be to add separate @JsonTypeInfo
for List
property, with use=Id.NONE
-- that should disable polymorphic handling.
So: for now I can't yet say whether this is something that may be changed, but I hope to look into it. Thank you for reporting the issue.
Yeah I worked around it by writing a custom deserializer for the entire list, which is basically a single line of extra code.
Perhaps this behavior could be documented in the mean time, because it isn't immediately obvious (to me at least) why contentUsing isn't compatible with polymorphic types.
@pdegoeje Yes, documentation improvement would be a good idea.
@pdegoeje could you share your workaround? I am facing the same issue without using
@JsonTypeInfo
, but with @JsonProperty
@james-woods I ended up with something like the following:
static class BarListDeserializer extends JsonDeserializer<List<Bar>> {
@Override
public List<Bar> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
List<Bar> barList = jsonParser.readValueAs(new TypeReference<List<Bar>>() { });
// do post-processing
return barList;
}
}
static class Foo {
// Use a custom deserializer for the entire list, instead of a per element deserializer.
@JsonDeserialize(using = BarListDeserializer.class)
public List<Bar> bars;
}
For my use case, registering a list of polymorphic typed objects so they could be referenced by constructors from objects later in the same json file, this was sufficient.
My use case is a parsing arrays full of null
values that I wanted to map to a "NULL" string value. As it turns out, writing a custom deserializer on the String
type is not enough for that. I think it needs to be one on List<String>
.
@james-woods That is odd -- defining method getNullValue(DeserializationContext ctxt)
on custom String
deserializer should work and there should be no need to override container deserializer. But this handling is distinct from regular deserialize
calls, so maybe that's what gave the impression?
@pdegoeje Actually, I am not quite sure what is being attempted here. If Bar
says it (and its subtypes, which is implied by Jackson) are to be handled using polymorphic type handling, that's what needs to happen. If you are trying to remove that, you should be overriding @JsonTypeInfo
with use=Id.NONE
. Adding deserializer should not change this aspect, since deserializers do not deal with type id resolution, TypeDeserializer
s do.
This is by design.
However, if the goal is to use custom deserializer beyond type handling I could see that as legit usage. Code just has to go via TypeDeserializer
route -- and that would seem potentially problematic.
But all in all looking simply at your example, I think the proper solution here is to add both:
static class Foo {
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
@JsonDeserialize(contentUsing = BarDeserializer.class)
public List<Bar> bars;
}
and that this should work as expected.
Added a failing test case that shows that override of @JsonTypeInfo
with Id.NONE
does not work as expected.
Can extend to handling of custom deserializer too, but I wanted to add test for one problem I understand clearly.
I was doing it wrong @cowtowncoder - I've adjusted the code from @pdegoeje to illustrate my problem and forgot to override the mentioned method. The following works - ok2
is emitted :) PEBCAK.
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class Test {
static class BarDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
System.out.println("ok");
return p.getValueAsString();
}
@Override
public String getNullValue(DeserializationContext ctxt){
System.out.println("ok2");
return "null";
}
}
static class Foo {
@JsonDeserialize(contentUsing = BarDeserializer.class)
public List<String> bars;
}
public static void main(String[] args) throws IOException {
Foo foo = new Foo();
ArrayList arrayList = new ArrayList();
arrayList.add(null);
arrayList.add(null);
foo.bars = arrayList;
ObjectMapper om = new ObjectMapper();
String json = om.writeValueAsString(foo);
System.out.println("json: " + json);
om.readValue(json, Foo.class);
}
}
@cowtowncoder Yes, adding @JsonTypeInfo(use=JsonTypeInfo.Id.NONE)
works as well.
Indeed, as I alluded to in my previous comment, my goal here was to be able to run some custom code after certain parts of the JSON file were loaded. Using a custom deserializer for this purpose is a hack which turns out to be a lot less code than either alternatives I explored: using full object graph persistence with manual fixup after deserialization or manually parsing the relevant parts with the streaming API.
To give some more context, I'm parsing a mini DSL which starts with a list of symbols with type information, followed by a list of functions (expression trees) which may reference any of the symbols. Because each expression node must be fully constructed after its constructor has completed, it must have access to the list of symbols during deserialization.
Eventually I might need to convert some of the parsing code to the streaming API anyway, but for now this solution lets jackson's objectmapper do all the heavy lifting, without a single line of actual custom parsing code. Which is pretty awesome :-)
Very cool, thank you for sharing more context. I agree that ability to use databinding for most things is good and streaming is much more work. So I hope I can figure out how to improve handling here.
The given workaround doesn't work for me when using @JsonDeserialize(using=...)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(
...
)
sealed class Criteria
class CriteriaStringDeserializer : JsonDeserializer<Criteria>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Criteria {
...
}
}
data class MyConfigClass(
@JsonTypeInfo(use=JsonTypeInfo.Id.NONE) @JsonDeserialize(using = CriteriaStringDeserializer::class) val criteria: Criteria? = null
)
Results in the error:
Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class Criteria]: missing type id property 'type' (for POJO property 'criteria')
My use case is I have a DSL that can be represented as JSON or as a string - I'm reading from a config file where I want to be able to read write criteria as a String (because it's a lot easier to write the string DSL that the JSON version). I've created a custom deserializer that parses the string and returns the POJO representation but Jackson is still trying to apply the JsonTypeInfo
annotation despite it being overridden on the property.
I have what I think is the same or similar use case, which I found moving from 2.10 to 2.12 or 2.13, although the workaround does work for all versions (2.10, 2.12, 2.13).
However, it is not at all intuitive that the serializer from @JsonSerialize is used, but the deserializer annotated on the same field is not. In addition, I found that if you register the deserializer on the ObjectMapper it also works, which provides a bit more merit that the deserializer defined in @JsonDeserialize should not be ignored.
Here is a simplified use case. There also seems to be some difference between 2.10 and later versions, although I have not been able to reproduce in a simple use case and the workaround works for both. As in, the original code is very similar and is currently working with 2.10.5 without adding @JsonTypeInfo on the field, so also wondering if there has been some change in how this handled since 2.10.
Thanks
@JsonTypeInfo(defaultImpl = Point.class, use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@Builder
@Getter
@EqualsAndHashCode
public static class Point {
private final int x;
private final int y;
private final int z;
public static Point from(String str) {
final String v = str.substring(1, str.length() - 1);
final String[] c = v.split(",");
return Point.builder()
.x(Integer.parseInt(c[0]))
.y(Integer.parseInt(c[1]))
.z(Integer.parseInt(c[2]))
.build();
}
@Override
public String toString() {
return "(" + x + "," + y + "," + z + ")";
}
}
public static class PointSerializer extends JsonSerializer<Point> {
@Override
public void serialize(Point p, JsonGenerator gen, SerializerProvider sp)
throws IOException {
gen.writeString(p.toString());
}
@Override
public void serializeWithType(Point value, JsonGenerator gen, SerializerProvider provider,
TypeSerializer typeSer)
throws IOException, JsonProcessingException {
this.serialize(value, gen, provider);
}
}
public static class PointDeserializer extends JsonDeserializer<Point> {
@Override
public Point deserialize(JsonParser jp, DeserializationContext dctx)
throws IOException, JsonProcessingException {
return Point.from(jp.getValueAsString());
}
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter(AccessLevel.PRIVATE)
@EqualsAndHashCode
public static class PointContainer {
@JsonProperty("name")
private String name;
// It works if this is uncommented
// @JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
@JsonSerialize(using = PointSerializer.class)
@JsonDeserialize(using = PointDeserializer.class)
@JsonProperty("pt")
private Point point;
@Builder
public PointContainer(String name, Point point) {
this.name = name;
this.point = point;
}
}
@Test
public void testRoundTrip() throws JsonProcessingException {
final ObjectMapper mapper = new ObjectMapper();
final SimpleModule module = new SimpleModule();
module.addDeserializer(Point.class, new PointDeserializer());
// It also works if this is uncommented
// mapper.registerModule(module);
final Point p = new Point(100, 200, 300);
final PointContainer pc = new PointContainer("test-meta", p);
final String ser = mapper.writeValueAsString(pc);
final PointContainer deser = mapper.readValue(ser, PointContainer.class);
Assert.assertEquals(pc, deser);
}