1

So I'm trying to parse a JSON using Jackson, but I'm having issues because the JSON does not use straight forward key-value pairs. Basically if I want to find the "title" I need to find the key "typename" with the value of "title" and then access the "value" key associated with that node to get the actual title. And that same pattern is used with all the JSON nodes and sub-nodes. I am struggling to figure out how to get Jackson to parse a Java object from that. Do I need to modify the JSON directly before Jackson can parse an object?

Here's an example from the JSON file:

        {"fields":[
            {
                "typeName":"title",
                "multiple":false,
                "typeClass":"primitive",
                "value":"Shapefile Dataset"
            },
            {
                "typeName":"author",
                "multiple":true,
                "typeClass":"compound",
                "value":[
                    {
                        "authorName":{
                            "typeName":"authorName",
                            "multiple":false,
                            "typeClass":"primitive",
                            "value":"Quigley, Elizabeth"
                        },
                        "authorAffiliation":{
                            "typeName":"authorAffiliation",
                            "multiple":false,
                            "typeClass":"primitive",
                            "value":"Harvard University"
                        }
                    }
                ]
            },
            {
                "typeName":"datasetContact",
                "multiple":true,
                "typeClass":"compound",
                "value":[
                    {
                        "datasetContactName":{
                            "typeName":"datasetContactName",
                            "multiple":false,
                            "typeClass":"primitive",
                            "value":"Quigley, Elizabeth"
                        },
                        "datasetContactAffiliation":{
                            "typeName":"datasetContactAffiliation",
                            "multiple":false,
                            "typeClass":"primitive",
                            "value":"Harvard University"
                        },
                        "datasetContactEmail":{
                            "typeName":"datasetContactEmail",
                            "multiple":false,
                            "typeClass":"primitive",
                            "value":"equigley@iq.harvard.edu"
                        }
                    }
                ]
            },
            {
                "typeName":"dsDescription",
                "multiple":true,
                "typeClass":"compound",
                "value":[
                    {
                        "dsDescriptionValue":{
                            "typeName":"dsDescriptionValue",
                            "multiple":false,
                            "typeClass":"primitive",
                            "value":"Dataset for shapefile"
                        }
                    }
                ]
            },
            {
                "typeName":"subject",
                "multiple":true,
                "typeClass":"controlledVocabulary",
                "value":[
                    "Earth and Environmental Sciences"
                ]
            },
            {
                "typeName":"depositor",
                "multiple":false,
                "typeClass":"primitive",
                "value":"Quigley, Elizabeth"
            },
            {
                "typeName":"dateOfDeposit",
                "multiple":false,
                "typeClass":"primitive",
                "value":"2015-07-13"
            }
        ]
    }
Paul
  • 13
  • 2
  • 1
    You want a customdeserializer. Some examples: https://www.baeldung.com/jackson-deserialization https://stackoverflow.com/questions/19158345/custom-json-deserialization-with-jackson https://dzone.com/articles/custom-json-deserialization-with-jackson http://www.davismol.net/2015/05/20/jackson-create-a-custom-json-deserializer-with-stddeserializer-and-jsontoken-classes/ – Jerry Jeremiah Feb 06 '19 at 23:18
  • Since basically all the node names are under the key "typename", am I going to have to do a lot of iterating through the nodes to find the node with the typename value that I want for each field? – Paul Feb 06 '19 at 23:50
  • I started making an answer (and the code worked) but then I realized I have no idea what data structure you want to represent the data. Do you just want a Map where the keys are path names (like for example: author.authorAffiliation='Harvard University' and dsDescription.dsDescriptionValue='Dataset for shapefile' ) or do you just want a Map where the keys are the leaf name (like for example: authorAffiliation='Harvard University' and dsDescriptionValue='Dataset for shapefile' ) or do you want something other than a Map ? – Jerry Jeremiah Feb 11 '19 at 22:40

2 Answers2

0

You can write custom deserialiser or use @JsonAnySetter annotation. You can do it in that way:

  1. Deserialise JSON to middle POJO structure which uses @JsonAnySetter annotation
  2. Convert middle POJO structure to Map
  3. Convert Map to destination POJO structure.

Deserialisation part and converting to Map could look like below:

class Fields {
    private Field[] fields;

    public Field[] getFields() {
        return fields;
    }

    public void setFields(Field[] fields) {
        this.fields = fields;
    }

    public Map<String, Object> toMap() {
        Map<String, Object> map = new HashMap<>();
        for (Field field : fields) {
            map.put(field.getTypeName(), field.getFieldValue().resolve());
        }

        return map;
    }

    @Override
    public String toString() {
        return "Fields{" +
                "fields=" + Arrays.toString(fields) +
                '}';
    }
}

class Field {

    private String typeName;
    private FieldValue fieldValue;

    @JsonAnySetter
    private void setValue(String propertyName, Object value) {
        FieldValueBuilder builder = new FieldValueBuilder();
        if (builder.accept(propertyName)) {
            this.fieldValue = builder.build(propertyName, value);
        }
    }

    public String getTypeName() {
        return typeName;
    }

    public void setTypeName(String typeName) {
        this.typeName = typeName;
    }

    public FieldValue getFieldValue() {
        return fieldValue;
    }

    public void setFieldValue(FieldValue fieldValue) {
        this.fieldValue = fieldValue;
    }

    @Override
    public String toString() {
        return "Field{" +
                "typeName='" + typeName + '\'' +
                ", fieldValue=" + fieldValue +
                '}';
    }
}

class FieldValueBuilder {

    private List<String> ignoreFields = Arrays.asList("multiple", "typeClass");

    public boolean accept(String propertyName) {
        return !ignoreFields.contains(propertyName);
    }

    public FieldValue build(String propertyName, Object value) {
        if (value instanceof String) {
            return new StringFieldValue(value.toString());
        }
        if (value instanceof List) {
            return deserialiseList((List) value);
        }

        System.out.println("Need to parse: key = " + propertyName + ", value = " + value);

        return null;
    }

    private FieldValue deserialiseList(List list) {
        if (list.isEmpty()) {
            return null;
        }

        // It is a tricky part. From example it looks like that value is always a single-element-array.
        // If not, handle it.
        Object item = list.get(0);

        if (item instanceof String) {
            return new StringFieldValue(item.toString());
        } else if (item instanceof Map) {
            List<Field> fields = new ArrayList<>();
            Map<String, Object> map = (Map<String, Object>) item;
            for (Object valueItem : map.values()) {
                if (valueItem instanceof Map) {
                    Map<String, Object> mapItem = (Map<String, Object>) valueItem;
                    Field field = new Field();
                    field.setTypeName(mapItem.get("typeName").toString());
                    field.setFieldValue(build("value", mapItem.get("value")));
                    fields.add(field);
                }
            }

            return new ListFieldValues(fields);
        } else {
            System.out.println(item);
        }

        return new NullFieldValue();
    }
}

interface FieldValue {

    Object resolve();
}

class StringFieldValue implements FieldValue {

    private final String value;

    public StringFieldValue(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    @Override
    public Object resolve() {
        return value;
    }

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

class ListFieldValues implements FieldValue {
    private final List<Field> fields;

    public ListFieldValues(List<Field> fields) {
        this.fields = fields;
    }

    public List<Field> getFields() {
        return fields;
    }

    @Override
    public Object resolve() {
        Map<String, Object> map = new HashMap<>();
        for (Field field : fields) {
            map.put(field.getTypeName(), field.getFieldValue().resolve());
        }

        return map;
    }

    @Override
    public String toString() {
        return "ListFieldValues{" +
                "fields=" + fields +
                '}';
    }
}

class NullFieldValue implements FieldValue {

    @Override
    public Object resolve() {
        return null;
    }
}

Final POJO structure could look like below:

class Book {
    private String subject;
    private Author author;
    private Description dsDescription;
    private String dateOfDeposit;
    private String depositor;
    private String title;
    private Contact datasetContact;

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public Author getAuthor() {
        return author;
    }

    public void setAuthor(Author author) {
        this.author = author;
    }

    public Description getDsDescription() {
        return dsDescription;
    }

    public void setDsDescription(Description dsDescription) {
        this.dsDescription = dsDescription;
    }

    public String getDateOfDeposit() {
        return dateOfDeposit;
    }

    public void setDateOfDeposit(String dateOfDeposit) {
        this.dateOfDeposit = dateOfDeposit;
    }

    public String getDepositor() {
        return depositor;
    }

    public void setDepositor(String depositor) {
        this.depositor = depositor;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Contact getDatasetContact() {
        return datasetContact;
    }

    public void setDatasetContact(Contact datasetContact) {
        this.datasetContact = datasetContact;
    }

    @Override
    public String toString() {
        return "Book{" +
                "subject='" + subject + '\'' +
                ", author=" + author +
                ", dsDescription=" + dsDescription +
                ", dateOfDeposit='" + dateOfDeposit + '\'' +
                ", depositor='" + depositor + '\'' +
                ", title='" + title + '\'' +
                ", datasetContact=" + datasetContact +
                '}';
    }
}

class Author {
    private String authorName;
    private String authorAffiliation;

    public String getAuthorName() {
        return authorName;
    }

    public void setAuthorName(String authorName) {
        this.authorName = authorName;
    }

    public String getAuthorAffiliation() {
        return authorAffiliation;
    }

    public void setAuthorAffiliation(String authorAffiliation) {
        this.authorAffiliation = authorAffiliation;
    }

    @Override
    public String toString() {
        return "Author{" +
                "authorName='" + authorName + '\'' +
                ", authorAffiliation='" + authorAffiliation + '\'' +
                '}';
    }
}

class Description {
    private String dsDescriptionValue;

    public String getDsDescriptionValue() {
        return dsDescriptionValue;
    }

    public void setDsDescriptionValue(String dsDescriptionValue) {
        this.dsDescriptionValue = dsDescriptionValue;
    }

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

class Contact {
    private String datasetContactEmail;
    private String datasetContactAffiliation;
    private String datasetContactName;

    public String getDatasetContactEmail() {
        return datasetContactEmail;
    }

    public void setDatasetContactEmail(String datasetContactEmail) {
        this.datasetContactEmail = datasetContactEmail;
    }

    public String getDatasetContactAffiliation() {
        return datasetContactAffiliation;
    }

    public void setDatasetContactAffiliation(String datasetContactAffiliation) {
        this.datasetContactAffiliation = datasetContactAffiliation;
    }

    public String getDatasetContactName() {
        return datasetContactName;
    }

    public void setDatasetContactName(String datasetContactName) {
        this.datasetContactName = datasetContactName;
    }

    @Override
    public String toString() {
        return "Contact{" +
                "datasetContactEmail='" + datasetContactEmail + '\'' +
                ", datasetContactAffiliation='" + datasetContactAffiliation + '\'' +
                ", datasetContactName='" + datasetContactName + '\'' +
                '}';
    }
}

Example usage of above code:

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JsonTest {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();


        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);

        Fields fields = mapper.readValue(jsonFile, Fields.class);
        System.out.println(fields);
        Map<String, Object> map = fields.toMap();
        System.out.println(map);
        System.out.println(mapper.convertValue(map, Book.class));
    }
}

Above code prints:

Fields{fields=[Field{typeName='title', fieldValue=StringFieldValue{value='Shapefile Dataset'}}, Field{typeName='author', fieldValue=ListFieldValues{fields=[Field{typeName='authorName', fieldValue=StringFieldValue{value='Quigley, Elizabeth'}}, Field{typeName='authorAffiliation', fieldValue=StringFieldValue{value='Harvard University'}}]}}, Field{typeName='datasetContact', fieldValue=ListFieldValues{fields=[Field{typeName='datasetContactName', fieldValue=StringFieldValue{value='Quigley, Elizabeth'}}, Field{typeName='datasetContactAffiliation', fieldValue=StringFieldValue{value='Harvard University'}}, Field{typeName='datasetContactEmail', fieldValue=StringFieldValue{value='equigley@iq.harvard.edu'}}]}}, Field{typeName='dsDescription', fieldValue=ListFieldValues{fields=[Field{typeName='dsDescriptionValue', fieldValue=StringFieldValue{value='Dataset for shapefile'}}]}}, Field{typeName='subject', fieldValue=StringFieldValue{value='Earth and Environmental Sciences'}}, Field{typeName='depositor', fieldValue=StringFieldValue{value='Quigley, Elizabeth'}}, Field{typeName='dateOfDeposit', fieldValue=StringFieldValue{value='2015-07-13'}}]}
{author={authorName=Quigley, Elizabeth, authorAffiliation=Harvard University}, subject=Earth and Environmental Sciences, dsDescription={dsDescriptionValue=Dataset for shapefile}, dateOfDeposit=2015-07-13, depositor=Quigley, Elizabeth, title=Shapefile Dataset, datasetContact={datasetContactEmail=equigley@iq.harvard.edu, datasetContactAffiliation=Harvard University, datasetContactName=Quigley, Elizabeth}}
Book{subject='Earth and Environmental Sciences', author=Author{authorName='Quigley, Elizabeth', authorAffiliation='Harvard University'}, dsDescription=Description{dsDescriptionValue='Dataset for shapefile'}, dateOfDeposit='2015-07-13', depositor='Quigley, Elizabeth', title='Shapefile Dataset', datasetContact=Contact{datasetContactEmail='equigley@iq.harvard.edu', datasetContactAffiliation='Harvard University', datasetContactName='Quigley, Elizabeth'}}
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
0

I added new answer because this approach is totally different then in first answer where @JsonAnySetter annotation is used.

There is also another way how we can parse given JSON. Using Jackson polymorphic type handling annotations: JsonTypeInfo and JsonSubTypes. To do that we need to create hierarchy which will represent primitive, compound and controlledVocabulary types.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "typeClass")
@JsonSubTypes({
        @JsonSubTypes.Type(value = PrimitiveField.class, name = "primitive"),
        @JsonSubTypes.Type(value = CompoundField.class, name = "compound"),
        @JsonSubTypes.Type(value = ControlledVocabularyField.class, name = "controlledVocabulary")
})
class Field<T> {
    protected String typeName;
    protected boolean multiple;
    protected T value;

    public String getTypeName() {
        return typeName;
    }

    public void setTypeName(String typeName) {
        this.typeName = typeName;
    }

    public boolean isMultiple() {
        return multiple;
    }

    public void setMultiple(boolean multiple) {
        this.multiple = multiple;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "{" +
                "typeName='" + typeName + '\'' +
                ", multiple=" + multiple +
                ", value=" + value +
                '}';
    }
}

class PrimitiveField extends Field<String> {
}

class CompoundField extends Field<List<Map<String, Field>>> {

    public Collection<Field> getFields() {
        if (value == null || value.isEmpty()) {
            return Collections.emptyList();
        }

        // Assume there is always one element
        Map<String, Field> object = value.get(0);

        return object.values();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("CompoundField{typeName='").append(typeName).append(", value=");
        getFields().forEach(sb::append);
        sb.append("}");

        return sb.toString();
    }
}

class ControlledVocabularyField extends Field<List<String>> {
}

We can test above solution like below:

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("path to json").getAbsoluteFile();


        ObjectMapper mapper = new ObjectMapper();
        Fields fields = mapper.readValue(jsonFile, Fields.class);
        System.out.println(fields);
    }
}

class Fields {
    private List<Field> fields;

    public List<Field> getFields() {
        return fields;
    }

    public void setFields(List<Field> fields) {
        this.fields = fields;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        fields.forEach(i -> sb.append(i).append(System.lineSeparator()));
        return sb.toString();
    }
}

Above code prints:

PrimitiveField{typeName='title', multiple=false, value=Shapefile Dataset}
CompoundField{typeName='author, value=PrimitiveField{typeName='authorName', multiple=false, value=Quigley, Elizabeth}PrimitiveField{typeName='authorAffiliation', multiple=false, value=Harvard University}}
CompoundField{typeName='datasetContact, value=PrimitiveField{typeName='datasetContactName', multiple=false, value=Quigley, Elizabeth}PrimitiveField{typeName='datasetContactAffiliation', multiple=false, value=Harvard University}PrimitiveField{typeName='datasetContactEmail', multiple=false, value=equigley@iq.harvard.edu}}
CompoundField{typeName='dsDescription, value=PrimitiveField{typeName='dsDescriptionValue', multiple=false, value=Dataset for shapefile}}
ControlledVocabularyField{typeName='subject', multiple=true, value=[Earth and Environmental Sciences]}
PrimitiveField{typeName='depositor', multiple=false, value=Quigley, Elizabeth}
PrimitiveField{typeName='dateOfDeposit', multiple=false, value=2015-07-13}

See also:

  1. Jackson annotations
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • Want to +1 this, but I don't have enough reputation points yet :P. Thanks! – Paul Mar 09 '20 at 12:20
  • @Paul, no problem. You can still accept this answer if it was helpful. Take a look at: [How does accepting an answer work?](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work) – Michał Ziober Mar 09 '20 at 12:38