2

Using an API I receive JSON like this (which are now saved to file):

[{
    "LEI": {
        "$": "549300Q82NZ9NYNMZT63"
    },
    "Entity": {
        "LegalName": {
            "$": "United Nerds in Collaboration of Random Nerdiness AB"
        },
        "LegalAddress": {
            "Line1": {
                "$": "BOX 155"
            },
            "City": {
                "$": "Alingsas"
            },
            "Region": {
                "$": "SE-O"
            },
            "Country": {
                "$": "SE"
            },
            "PostalCode": {
                "$": "44123"
            }
        },
        "HeadquartersAddress": {
            "Line1": {
                "$": "BOX 155"
            },
            "City": {
                "$": "Alingsas"
            },
            "Region": {
                "$": "SE-O"
            },
            "Country": {
                "$": "SE"
            },
            "PostalCode": {
                "$": "44123"
            }
        },
        "BusinessRegisterEntityID": {
            "@register": "SE001",
            "$": "5568557184"
        },
        "LegalJurisdiction": {
            "$": "SE"
        },
        "LegalForm": {
            "$": "PRIVATA AKTIEBOLAG"
        },
        "EntityStatus": {
            "$": "ACTIVE"
        }
    },
    "Registration": {
        "InitialRegistrationDate": {
            "$": "2016-06-23T01:48:45.025Z"
        },
        "LastUpdateDate": {
            "$": "2016-06-23T01:48:44.945Z"
        },
        "RegistrationStatus": {
            "$": "ISSUED"
        },
        "NextRenewalDate": {
            "$": "2017-06-21T06:32:03.821Z"
        },
        "ManagingLOU": {
            "$": "EVK05KS7XY1DEII3R011"
        },
        "ValidationSources": {
            "$": "PARTIALLY_CORROBORATED"
        }
    }
}]

I would like to get Java Object out of these. I have already created the Java Objects out of an xsd file provided. The code I'm running is:

public static void toJava() {
    ObjectMapper mapper = new ObjectMapper();
    try {
        File json = new File("C:\\temp\\JSON.json");
        LEIRecordType[] type = mapper.readValue(json, LEIRecordType[].class);
    } catch (JsonEOFException ex) {
        ex.printStackTrace();
    } catch (JsonMappingException ex) {
        ex.printStackTrace();
    } catch (IOException ex) {
        ex.printStackTrace();
    }
}

Which creates these Exceptions:

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "LEI" (class   org.leiroc.data.schema.leidata._2014.LEIRecordType), not marked as ignorable (5 known properties: "lei", "registration", "entity", "nextVersion", "extension"])
 at [Source: (File); line: 3, column: 14] (through reference chain: java.lang.Object[][0]->org.leiroc.data.schema.leidata._2014.LEIRecordType["LEI"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:60)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:822)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:1152)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1567)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1545)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:293)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:195)
at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:21)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4001)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2890)
at Test.JSONParser.toJava(JSONParser.java:38)
at Test.JSONParser.main(JSONParser.java:29)

LEIRecordType looks like this:

package org.leiroc.data.schema.leidata._2014;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "LEIRecordType", propOrder = {"lei", "entity", "registration", "nextVersion", "extension"})
public class LEIRecordType {

    @XmlElement(name = "LEI", required = true)
    protected String lei;

    @XmlElement(name = "Entity", required = true)
    protected EntityType entity;

    @XmlElement(name = "Registration", required = true)
    protected RegistrationType registration;

    @XmlElement(name = "NextVersion")
    protected LEIRecordNextVersionType nextVersion;

    @XmlElement(name = "Extension")
    protected ExtensionType extension;

    public String getLEI() {
        return this.lei;
    }

    public void setLEI(String paramString) {
        this.lei = paramString;
    }

    public EntityType getEntity() {
        return this.entity;
    }

    public void setEntity(EntityType paramEntityType) {
        this.entity = paramEntityType;
    }

    public RegistrationType getRegistration() {
        return this.registration;
    }

    public void setRegistration(RegistrationType paramRegistrationType) {
        this.registration = paramRegistrationType;
    }

    public LEIRecordNextVersionType getNextVersion() {
        return this.nextVersion;
    }

    public void setNextVersion(LEIRecordNextVersionType paramLEIRecordNextVersionType) {
        this.nextVersion = paramLEIRecordNextVersionType;
    }

    public ExtensionType getExtension() {
        return this.extension;
    }

    public void setExtension(ExtensionType paramExtensionType) {
        this.extension = paramExtensionType;
    }
}

I understand that the problem is that jackson is locking for an Java Object called LEI, with an variable called "$". But there is none. The organisations help service says:

"The "$" object always reproduces the simple content (i.e. not the attributes, child nodes etc.) of the corresponding XML element. The "$" object should always be typed as a JSON string where applicable."

But as I understand this is not JSON standard.

My question is: Is there any way to get jackson to parse this as LEI = "549300Q82NZ9NYNMZT63" etc. instead of and object LEI with an variable "$"? Have been stuck on this for the better part of a day.

@UPDATE This JSON format is apparently called "The BadgerFish convention", accoring to customer services.

Erik
  • 21
  • 4

3 Answers3

1

As the $ object is always a String, you can create a custom deserializer for Strings that handles the BadgerFish wrapper objects.

This deserializer checks if there is a BadgerFish wrapper object around a String value and unwraps it. Normal String values are deserialized as usual.

public class BadgerFishDeserializer extends StdDeserializer<String> {

    private static final long serialVersionUID = 1L;

    private static final SerializedString BADGER_FISH_FIELD_NAME = new SerializedString("$");

    public BadgerFishDeserializer() {
        super(String.class);
    }

    @Override
    public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        // do we have a wrapper object?
        if (jp.isExpectedStartObjectToken()) {          
            // check if first field name is equal to '$'
            if (!jp.nextFieldName(BADGER_FISH_FIELD_NAME)) {
                ctxt.reportInputMismatch(String.class, "Expected BadgerFish field name '$', but got '%s'", jp.getCurrentName());
            }
            jp.nextValue();  // proceed to the actual value
            String value = jp.getValueAsString();  // read value as string
            jp.nextToken();  // consume END_OBJECT of wrapper object
            return value;
        }
        // else: just return string value
        return jp.getValueAsString();
    }

}

Finally register the module on your Jackson ObjectMapper instance:

SimpleModule module = new SimpleModule();
module.addDeserializer(String.class, new BadgerFishDeserializer());
mapper.registerModule(module);

Note: If you only want some properties to be unwrapped, you could create a custom annotation and use a BeanDeserializerModifier to check for the annotation and then provide a deserializer that handles the wrapper objects.

Some food for thought:

  • Create annotation
  • Modify the deserializer to always expect wrapper objects (fail on plain Strings)
  • Create a DeserializerModifier
  • Register the DeserializerModifier on ObjectMapper

The difficult part:

public class BadgerFishDeserializerModifier extends BeanDeserializerModifier {

    @Override
    public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
        Iterator<SettableBeanProperty> props = builder.getProperties();
        while (props.hasNext()) {
            SettableBeanProperty prop = props.next();
            if (prop.getAnnotation(MyAnnotation.class) != null) {   
                builder.addOrReplaceProperty(prop.withValueDeserializer(new BadgerFishDeserializer()), true);
            }
        }
        return builder;
    }

}
Christian
  • 295
  • 2
  • 6
0

This has been very helpful! I ended up having to do a special deserializer for both String and XMLGregorianCalendar aswell. But the problem does not stop there. BadgerFish takes the @XMLAttributes from the generated classes and sets them as @value instead of "value". As for example:

"BusinessRegisterEntityID": {
        "@register": "SE001",
        "$": "5568557184"
    }

So is their any way to also custom the field name back to the original? Currently I now get this exception:

Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "@register" (class leigen.BusinessRegisterEntityIDType), not marked as ignorable (2 known properties: "value", "register"])
at [Source: (File); line: 44, column: 27] (through reference chain: java.lang.Object[][0]->leigen.LEIRecordType["Entity"]->leigen.EntityType["BusinessRegisterEntityID"]->leigen.BusinessRegisterEntityIDType["@register"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:60)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:822)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:1152)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1567)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1545)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:293)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:136)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:287)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:136)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:287)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:195)
at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:21)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4001)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2890)
at Test.JSONParser.toJava(JSONParser.java:47)
at Test.JSONParser.main(JSONParser.java:28)

I'm able to bypass this by putting a "ignore" (DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES )in the config like in my now current code here:

    public static LEIRecordType[] toJava(File json) throws JsonParseException, JsonMappingException, IOException {
    ObjectMapper mapper = new ObjectMapper();
    SimpleModule module = new SimpleModule();
    module.addDeserializer(String.class, new BadgerFishStringDeserializer());
    module.addDeserializer(XMLGregorianCalendar.class, new BadgerFishXMLGregorianCalendarDeserializer());
    module.addDeserializer(NameType.class, new BadgerFishNameTypeDeserializer());
    mapper.registerModule(module);
    mapper.registerModule(new JaxbAnnotationModule()); // To be able to read JAXB annotations.

    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    return mapper.readValue(json, LEIRecordType[].class);
}   

But this only sets the values starting with @ as null. Is there another way to solve this? Otherwise I would have to write a custom deserializer for all my generated classes, which is about 25 (and there could always be more in the next version). I have already done one for NameType, but for other example more are needed.

Erik
  • 21
  • 4
  • I hope you have figured it out meanwhile, but just in case: you can use `@JsonProperty("@register")` on your `register` field. – Christian Aug 06 '18 at 12:18
0

I think the clear solution is using @JsonProperty("$"):

ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
List<LEIModel> leiList = Arrays.asList(mapper.readValue(response.toString(), LEIModel[].class));

The LEIModel class:

public class LEIModel {
   @JsonProperty("Entity")
   private Entity entity;

   public Entity getEntity() {
       return entity;
   }

   public void setEntity(Entity entity) {
       this.entity = entity;
   }
}

The Entity class:

public class Entity {
   @JsonProperty("LegalName")
   private LegalName legalName;

   public LegalName getLegalName() {
       return legalName;
   }

   public void setLegalName(LegalName legalName) {
       this.legalName = legalName;
   }
}

The LegalName class:

public class LegalName {
   @JsonProperty("$")
   private String value;

   public String getValue() {
       return value;
   }

   public void setValue(String value) {
       this.value = value;
   }
}
Amin
  • 138
  • 7