Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson, deserialize property based on another property (dependent property)

Tags:

java

json

jackson

Using Jackson, is there a way to deserialize a proprty that depends on the value of another property?

if i have this json {"foo":"a","bar":"b"} i'd like to deserialize it to the Test class below as Test [foo=a, bar=b_a], where bar is the value of the json property "bar" and the value of the property "foo".

Of course this is a trivial example, the real deal would be to deserialize a datamodel entity: {"line":"C12", "machine": {"line":"C12", "code":"A"}} machine.line and line are always the same, and i'd like to express it like this: {"line":"C12", "machine": "A"}

import java.io.IOException;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

public abstract class Main{

    private static class Test {
        @JsonProperty
        private String foo;
        @JsonProperty
        @JsonDeserialize(using = CustomDeserializer.class)
        private String bar;

        // ...other fields to be deserialized with default behaviour

        private Test() {
        }

        public Test(String a, String bar) {
            this.foo = a;
            this.bar = bar;
        }

        @Override
        public String toString() {
            return "Test [foo=" + foo + ", bar=" + bar + "]";
        }
    }

    private static class CustomDeserializer extends StdDeserializer<String> {

        protected CustomDeserializer() {
            super(String.class);
        }

        @Override
        public String deserialize(JsonParser p, DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            String foo = //how to get foo property?
            String value = p.getValueAsString();
            if (!foo.isEmpty()) {
                return value + "_" + foo;
            } else {
                return value;
            }
        }

    }

    public static void main(String[] args) throws IOException {

        ObjectMapper mapper = new ObjectMapper();

        Test foo2 = mapper.readValue("{\"foo\":\"a\",\"bar\":\"b\"}", Test.class);
        System.out.println(foo2); // Test [foo=a, bar=b_a]

    }

}
like image 650
lelmarir Avatar asked Oct 21 '25 11:10

lelmarir


2 Answers

One way to solve your problem is specify a custom deserializer that involves your Test class instead of your string field because the deserialization of your property is based on the value of another property:

public class CustomDeserializer extends JsonDeserializer<Test> {}

@JsonDeserialize(using = CustomDeserializer.class)
public class Test {}

Then you can deserialize your object reading the JsonNode tree built from your input string:

public class CustomDeserializer extends JsonDeserializer<Test> {

    @Override
    public Test deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        String foo = node.get("foo").asText();
        String bar = node.get("bar").asText();
        if (!foo.isEmpty()) {
            bar = (bar + '_' + foo);
        }
        return new Test(foo, bar);
    }
    
}

//your example
public class Main {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        Test foo2 = mapper.readValue("{\"foo\":\"a\",\"bar\":\"b\"}", Test.class);
        System.out.println(foo2); // Test [foo=a, bar=b_a]
    }
}

like image 130
dariosicily Avatar answered Oct 23 '25 00:10

dariosicily


I got a similar problem today and I wanted to share my solution. So instead of using a @JsonDeserialize, I use a @JsonCreator on the parent object with a package private constructor to accept the "raw" properties and then I can process this data and return better objects.

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

class Scratch {
    public static void main(String[] args) throws Exception{
        final var testData = "{\"foo\":\"a\",\"bar\":\"b\"}";
        final var mapper = new ObjectMapper();
        final var testObj = mapper.readValue(testData, Test.class);

        System.out.println(testObj); // Test[foo=a, bar=a_b]
    }

    record Test (
            String foo,
            String bar
    ){
        @JsonCreator Test(
                @JsonProperty("foo") String foo,
                @JsonProperty("bar") String bar,
                @JsonProperty("_dummy") String _dummy // extra param for the constructor overloading
        ) {
            this(foo, deserializeBar(foo, bar));
        }

        private static String deserializeBar(String foo, String bar) {
            if (foo == null || foo.isEmpty()) {
                return bar;
            }

            return "%s_%s".formatted(foo, bar);
        }
    }
}
like image 40
Francis Avatar answered Oct 23 '25 00:10

Francis