Skip to content

Attributes and nested de-serialization #3000

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

Closed
lburgazzoli opened this issue Dec 23, 2020 · 3 comments
Closed

Attributes and nested de-serialization #3000

lburgazzoli opened this issue Dec 23, 2020 · 3 comments

Comments

@lburgazzoli
Copy link

In a deserializer I'm writing I need to access to some attributes but it looks like those attributes are not propagated to nested deserializations.

For demonstration purpose I have created the following two classes:

@JsonDeserialize(using = Foo.Deserializer.class)
public static class Foo {
    Bar bar;
    String message;

    public static class Deserializer extends StdDeserializer<Foo> {
        public Deserializer() {
            super(Foo.class);
        }

        @Override
        public Foo deserialize(JsonParser p, DeserializationContext ctx) 
                throws IOException {
            Foo foo = new Foo();
            foo.message = (String)ctx.getAttribute("message");

            JsonNode node = p.getCodec().readTree(p);
            JsonNode bar = node.get("bar");

            if (bar != null) {
                foo.bar = p.getCodec().treeToValue(bar, Bar.class);
            }

            return foo;
        }
    }
}

@JsonDeserialize(using = Bar.Deserializer.class)
public static class Bar {
    String id;
    String message;

    public static class Deserializer extends StdDeserializer<Bar> {
        public Deserializer() {
            super(Bar.class);
        }

        @Override
        public Bar deserialize(JsonParser p, DeserializationContext ctx) 
                throws IOException {
            Bar bar = new Bar();
            bar.message = (String)ctx.getAttribute("message");

            JsonNode node = p.getCodec().readTree(p);
            JsonNode id = node.get("id");

            if (id != null) {
                bar.id = id.textValue();
            }

            return bar;
        }
    }
}

If I deserialize Foo setting the attributes to the deserialization config, like:

ObjectMapper mapper = new ObjectMapper();
mapper.getDeserializationConfig().withAttribute("message", "test");

Foo foo = mapper.readValue(
    "{ \"bar\": { \"id\": \"1\"} }",
    Foo.class);

At this point, neither Foo nor Bar have the message field set with the message attribute.

If instead I set the attributes on the reader, like:

ObjectMapper mapper = new ObjectMapper();

Foo foo = mapper.readerFor(Foo.class)
    .withAttribute("message", "test")
    .readValue("{ \"bar\": { \"id\": \"1\"} }");

Then Foo has the expected message set whereas Bar not.

I'm pretty sure I'm doing something wrong but I have not found a relevant documentation about how to properly sue attributes so any help would be really appreciated.

@lburgazzoli lburgazzoli added the to-evaluate Issue that has been received but not yet evaluated label Dec 23, 2020
@cowtowncoder
Copy link
Member

Ok there are two different parts to the problem. Starting with changing DeserializationConfig for mapper...
Jackson methods that start with word with() will not modify instance they are called on but will either return unmodified instance as-is (if no change would be needed), or construct and return a new instance.
So DeserializationConfig.with(...) method, when adding a new attribute, will construct and return a new config instance: that will need to be set with mapper: code, as-is, would not do that.
So for Jackson 2.x you'd use:

ObjectMapper mapper = new ObjectMapper();
mapper.setConfig(mapper.getDeserializationConfig().withAttribute("message", "test"));

Now: second part wrt ObjectReader is something that is a limitation of Jackson 2.x. Call:

p.getCodec().treeToValue(bar, Bar.class);

will go to ObjectMapper which will create a new context (sort of equivalent to creating new ObjectReader and ObjectWriter -- both with newly created configuration objects) which does not have attribute set (since it was not set for mapper but just for reader).

The proper solution actually would be not to use treeToValue() but fetch deserializer via DeserializationContext, use it. Or, unless there is something special (wrt polymorphic handling) use convenience method readValue() -- although given that you have JsonNode, there's little bit of gymnastic

            if (bar != null) {
                // .traverse() will create JsonParser: not 100% if "parser.nextToken()" needs to be called before
                // using here:
                foo.bar = ctx.readValue(bar.traverse(), Bar.class);
            }

but that would fully retain appropriate DeserializationContext with attributes. It'd be little bit more efficient too.

So: I would recommend changing deserialization like this and then you can decide whether to set default attributes for ObjectMapper or just ObjectReader.

@cowtowncoder cowtowncoder removed the to-evaluate Issue that has been received but not yet evaluated label Dec 23, 2020
@cowtowncoder
Copy link
Member

One other minor improvement suggestion: DeserializationContext has readTree() method, added in Jackson 2.10: this is preferable to p.getCodec().readTree() -- not a big difference but also bit more streamlined.

@cowtowncoder
Copy link
Member

I hope above helps solving the issue: there isn't much databind can do (in 2.x -- 3.0 actually does have improvements that will keep attributes via ObjectRead/WriteContext)

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