3

I have a JSONNode object that can contain any JSON content. Example :

{
    "fieldA": "aStringValue",
    "fieldB": 10,
    "fieldC": {
        "TypeAFieldA": "aValue"
    },
    "fieldD": {
        "TypeBFieldA": "aValue",
        "TypeBFieldB": {
            "TypeCFieldA": "aValue",
            "TypeCFieldB": "bValue"
        }
    }
}

I want to deserialize each JSON field in this string into different types of java objects as below :

fieldA -> String object
fieldB -> int 
fieldC -> TypeA object
fieldD -> TypeB object

Assume that I know the class type that each field should deserialize into. What is the best and most optimal way to go about this?

Edit: To further clarify my requirement :

The approach I have thought of is I have created objects for TypeA, TypeB, TypeC etc and have annotated them with relevant JsonPropery annotations. What I am unclear about is how do I go about deserializing each field individually? For this I would need to extract the json string from the JsonNode one by one and run through an object mapper with the relevant class type?

Example: To deserialize "fieldC" and its value into a class of TypeC, don't I have to do something like :

  1. Extract full Json string :

    String jsonString = "fieldC": { "TypeAFieldA": "aValue" }";

  2. Run it through an object mapper:

    mapper.readValue( jsonString, TypeC.class );

How do I extract the full json string for each field by looping through the JsonNode? Is this the most optimal way to go about this?

1
  • No they can be any name. I want the solution to be generic for both field names and values. Commented Apr 8, 2016 at 13:44

3 Answers 3

3

You can do something like this:

ObjectMapper mapper = new ObjectMapper();
JsonNode actualObj = mapper.readTree(json);

JsonNode fieldA = actualObj.get("fieldA");
String fieldAObj = fieldA.asText();

JsonNode fieldB = actualObj.get("fieldB");
Integer fieldBObj = fieldB.asInt();

JsonNode fieldC = actualObj.get("fieldC");
//if you really want json string of fieldC just use fieldC.toString()
TypeA fieldCObj = mapper.treeToValue(fieldC, TypeA.class);

JsonNode fieldD = actualObj.get("fieldD");
TypeB fieldDObj = mapper.treeToValue(fieldD, TypeB.class);

And here is 100% generic version:

JsonNode actualObj = mapper.readTree(json);
Iterator<Map.Entry<String, JsonNode>> values = actualObj.fields();

Object field;
while (values.hasNext()){
    Map.Entry<String, JsonNode> entry = values.next();
    String key = entry.getKey();
    JsonNode value = entry.getValue();

    if(value.canConvertToInt()){
        // Integer
        field = value.asInt();
    }else if(value.isTextual()){
        // String
        field = value.asText();
    }else{
        try {
            field  = mapper.treeToValue(value, TypeA.class);
        }catch (Exception e){
            field  = mapper.treeToValue(value, TypeB.class);
        }
    }
    System.out.println(key + " => "+ field);
}

Or you can use parent object with @JsonAnySetter and put all the logic where you determine object type and create object instances in this setter. Here is demo

public static class Data{
    private  HashMap<String,Object> data = new HashMap<String, Object>();

    @JsonAnyGetter
    public HashMap<String, Object> getValues(){
        return data;
    }

    @JsonAnySetter
    public void setValue(String key, JsonNode value) {
        // value.toString() is json string of each field
        Object resultObj = "";

        if (value.canConvertToInt()) {
            resultObj = String.valueOf(value);
        } else if (value.isTextual()) {
            resultObj = String.valueOf(value);
        } else if (value.has("TypeAFieldA")) {
            try {
                resultObj = mapper.treeToValue(value, TypeA.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else if (value.has("TypeBFieldB")) {
            try {
                resultObj = mapper.treeToValue(value, TypeB.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        System.out.println(key + " " + resultObj);
        // can use key - resultObj pair any way you want
        //for example add it to hashmap or multiple hashmaps for each class type
        data.put(key, resultObj);
    }
}

Test Code:

public class Main {
private static ObjectMapper mapper = new ObjectMapper();
private static final String json = "{\n" +
        "    \"fieldA\": \"aStringValue\",\n" +
        "    \"fieldB\": 10,\n" +
        "    \"fieldC\": {\n" +
        "        \"TypeAFieldA\": \"aValue\"\n" +
        "    },\n" +
        "    \"fieldD\": {\n" +
        "        \"TypeBFieldA\": \"aValue\",\n" +
        "        \"TypeBFieldB\": {\n" +
        "            \"TypeCFieldA\": \"aValue\",\n" +
        "            \"TypeCFieldB\": \"bValue\"\n" +
        "        }\n" +
        "    }\n" +
        "}";

public static void main(String[] args) throws IOException, JSONException {
    Data data =  mapper.readValue( json, Data.class );
    String json =  mapper.writeValueAsString(data);
    System.out.println(json);
}

public static  class TypeA {
    @JsonProperty("TypeAFieldA")
    private String TypeAFieldA;

    @Override
    public String toString() {
        return "TypeA{" +
                "TypeAFieldA='" + TypeAFieldA + '\'' +
                '}';
    }
}

public static  class TypeB {
    @JsonProperty("TypeBFieldA")
    private String TypeBFieldA;

    @JsonProperty("TypeBFieldB")
    private TypeC TypeBFieldB;

    @Override
    public String toString() {
        return "TypeB{" +
                "TypeBFieldA='" + TypeBFieldA + '\'' +
                ", TypeBFieldB=" + TypeBFieldB +
                '}';
    }
}

public static  class TypeC {
    @JsonProperty("TypeCFieldA")
    private String TypeCFieldA;

    @JsonProperty("TypeCFieldB")
    private String TypeCFieldB;

    @Override
    public String toString() {
        return "TypeC{" +
                "TypeCFieldA='" + TypeCFieldA + '\'' +
                ", TypeCFieldB='" + TypeCFieldB + '\'' +
                '}';
    }
}
}

Result:

fieldA aStringValue
fieldB 10
fieldC TypeA{TypeAFieldA='aValue'}
fieldD TypeB{TypeBFieldA='aValue', TypeBFieldB=TypeC{TypeCFieldA='aValue', TypeCFieldB='bValue'}}
Sign up to request clarification or add additional context in comments.

Comments

2

Inspired by the solutions posted here, I was able to come up with my own implementation for the problem.

I wrote a function that takes in a JsonNode, and a java.lang.reflect.Type parameter. This function would check the node for each primitive and non primitive data type that I will be using in my application and deserialize it into the appropriate type.

/**
     * This function takes in a JSON node, a type info and converts the JSON into 
     * the given type.
     * @param node - node to deserialize
     * @param typeInfo - data type to deserialize into
     * @throws JsonMappingException
     * @throws JsonParseException
     * @throws IOException
     */
    private void deserializeNode ( JsonNode node, Type typeInfo ) throws JsonMappingException, JsonParseException, IOException {

        Object deserializedValue = null;

        if ( node.isDouble()   ) {
            deserializedValue = node.asDouble();

        } else if ( node.isInt() ) {
            deserializedValue = node.asInt();

        } else if ( node.isLong() ) {
            deserializedValue = node.asLong();

        } else if ( node.isBoolean() ) {
            deserializedValue = node.asBoolean();

        } else if ( node.isArray() ) {
            //Json array is translated into a Java List. If this is a known type, it will translate
            //into a List<Type> instance.
            CollectionType collectionType = this.getActualTypeOfCollection( typeInfo );
            deserializedValue = mapper.readValue( node.toString(),  collectionType );

        } else if ( node.isObject() ) {
            JavaType objectType = mapper.getTypeFactory().constructType( typeInfo );
            deserializedValue = mapper.readValue( node.toString(), objectType );

        } else if ( node.isTextual() ) {
            deserializedValue = node.asText();

        } 

        this.deserializedValues.add( deserializedValue );

    }


    /**
     * This function returns the actual collection type of a generic parameter.
     * I.e. It returns the proper Collection data complete with the generic type so
     * that Jackson could determine the proper type to deserialize the field into.
     * @param genericParameterType - java parameter type
     * @return Jackson collection type
     */
    private CollectionType getActualTypeOfCollection ( Type genericParameterType ) {

        CollectionType collectionType = null;

        if(genericParameterType instanceof ParameterizedType){

            ParameterizedType aType = (ParameterizedType) genericParameterType;
            Type[] parameterArgTypes = aType.getActualTypeArguments();
            for ( Type parameterArgType : parameterArgTypes ) {
                collectionType = mapper.getTypeFactory().constructCollectionType(List.class, (Class<?>) parameterArgType ) ;
                break;
            }
        }

        return collectionType;      
    }

Comments are welcome on the pros/cons of this approach.

Comments

1

Create a Java class, say JsonNode, and define all the given attributes with the known data types and their getter and setter methods. Then annotate each attribute with @JsonProperty attribute.

The complex objects can be defined as nested classes and defined in your JsonNode class as an attribute object of the particular nested class. The object class in turn can have attributes annotated with @JsonProperty.

Something like this -

class JsonNode {
        @JsonProperty("fieldA")
        private String fieldA;

        @JsonProperty("fieldB")
        private int fieldB;

        @JsonProperty("fieldC")
        private TypeA fieldC;

        @JsonProperty("fieldD")
        private TypeB fieldB;

        getters and setters...

        class TypeA {
            @JsonProperty("innerFieldA")
            private String innerFieldA;

            getters, setters
        }

        class TypeA {
            @JsonProperty("innerFieldB")
            private String innerFieldB;

            getters, setters
        }

}

3 Comments

Hi, Thanks for your asnwer. I have the class annotations part figured out. I have updated the original question to clarify my requirement since it was ambiguous earlier.
You can pass JsonNode.class as parameter to mapper.read value . The method will return an object of JsonNode. Each attribute value can be obtained via its getter method.
Actually my requirement is to extract individual fields out of the JsonNode. For example to deserialize "fieldC" and its value into a class of TypeC, I need the full Json string : "fieldC": { "TypeAFieldA": "aValue" }" , extracted from the original JsonNode. and then rit through an object mapper to create an object of TypeC: mapper.readValue( jsonString, TypeC.class );

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.