0

I am using Jackson for de/serialization in my app.

I have a situation where I need to convert a JSON string to one of my 3 classes. In case the string can't be converted to either one of 3 classes, it will considered to be an unrecognized case.

However, if the schema of json string and the provided class in mapper.readValue(jsonString,MyClass1.class) does not match, it throws an UnrecognizedPropertyException.

Currently I am using something like below, but it seems to be pretty messy.

try {
    obj = mapper.readValue(jsonString, MyClass1.class);             
} catch (UnrecognizedPropertyException e1) {
    try {
        obj = mapper.readValue(jsonString, MyClass2.class);
    } catch (UnrecognizedPropertyException e2) {
        try {
            obj = mapper.readValue(jsonString, MyClass3.class);
        } catch (Exception e) {
            //handle unrecognized string
        }
    } catch (Exception e) {
        //handle unrecognized string
    }
} catch (Exception e) {
    //handle unrecognized string
}

Is this how it needs to be done or is there any other alternative? Is there any way to configure the mapper to return null in case of unrecognized properties, as that would result in creating a simple series if blocks instead of nested try-catch blocks?

Sayan Pal
  • 4,768
  • 5
  • 43
  • 82

3 Answers3

1

You can try this method to do deserialization thing. this will return null on UnrecognizedPropertyException:

private <T> T deserialize(ObjectMapper mapper, Class<T> type, String jsonString) {
        T t = null;
        try {
            t = mapper.readValue(jsonString, type);
        } catch (UnrecognizedPropertyException  e) {
            //handle unrecognized string
        }catch (IOException  e) {
            //handle under errors
        }
        return t;
    }
Sachin Gupta
  • 7,805
  • 4
  • 30
  • 45
  • Thank you for your answer. But it seems that configuring ignoring the unknown properties just create an empty instance of provided class via `Class>`, with default values, rather than creating `null` instance of the class. Hope this explanation is clear. – Sayan Pal Apr 05 '17 at 08:46
  • 1
    I have updated my answer, you can create a generic method to do this kind of thing. – Sachin Gupta Apr 05 '17 at 09:25
0

If jsonString is generated by you, you can consider to add type info and then use it to convert deserialized object. You could refer to this post for how to do it.

If jsonString is generated by other services beyond your control, then there's no type info you can get so you can only try it one by one, @Sachin Gupta's answer would be a nice choice.

I'd like to provide an additional option: define an all-in-one entity including all fields of MyClass1, MyClass2 and MyClass3, and make MyClass1, MyClass2 and MyClass3 be separated wrapper and only expose related fields for each. Code as follows:

Class AllInOne:

public class AllInOne {
    protected String a;
    protected String b;
    protected String c;

    public A asA() {
        return new A(this);
    }

    public B asB() {
        return new B(this);
    }

    public C asC() {
        return new C(this);
    }
}

Class A:

public class A {
    private AllInOne allInOne;

    public A(AllInOne allInOne) {
        this.allInOne = allInOne;
    }

    public String getA() {
        return allInOne.a;
    }
}

Class B:

public class B {
    private AllInOne allInOne;

    public B(AllInOne allInOne) {
        this.allInOne = allInOne;
    }

    public String getB() {
        return allInOne.b;
    }
}

Class C:

public class C {
    private AllInOne allInOne;

    public C(AllInOne allInOne) {
        this.allInOne = allInOne;
    }

    public String getC() {
        return allInOne.c;
    }
}

Test code:

public class Main {
    public static void main(String[] args) throws IOException {
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

        String jsonA = "{\"a\":\"a value\"}";
        String jsonB = "{\"b\":\"b value\"}";
        String jsonC = "{\"c\":\"c value\"}";

        needTypeA(om.readValue(jsonA, AllInOne.class).asA());
        needTypeB(om.readValue(jsonB, AllInOne.class).asB());
        needTypeC(om.readValue(jsonC, AllInOne.class).asC());
    }

    private static void needTypeA(A a) {
        System.out.println(a.getA());
    }

    private static void needTypeB(B b) {
        System.out.println(b.getB());
    }

    private static void needTypeC(C c) {
        System.out.println(c.getC());
    }
}

With implementation like this, we erased the specific type info at deserialization step, and bring it back at the moment we really need/use it. And as you can see there's not too much extra code, because what we actually did is just moving all fields declaration together, and added couple methods.

Notes:

  • I declare fields in AllInOne to be protected, putting all POJO class in the same package will make A, B and C be able to access them directly, but not for other classes outside.
  • Setting om.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); to make jackson deserialize by field, so that we can remove duplicate setter and getter from AllInOne class
  • If you do need to know the type info, you could add methods like isA inside AllInOne based on the fields info
Community
  • 1
  • 1
shizhz
  • 11,715
  • 3
  • 39
  • 49
0

If json contains some define property, than you can try to use @JsonTypeInfo and @JsonSubTypes. Classes MyClass1, ... must implement this interface. Also I don`t remember exactly how to map unknown implementations to null.

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.EXISTING_PROPERTY, // level of define property
        property = <property_name>,
        visible = true,
        defaultImpl = NoClass.class)
@JsonSubTypes({@JsonSubTypes.Type(value = <interface-impl>.class, name = <property_value>)})
private <interface> value;
// getters and setters
ZhenyaM
  • 676
  • 1
  • 5
  • 15