Jackson XML serialization does not produce desired result - java

The goal is to create a class to serialize to the following xml:
<ParticipantID code="AA">participant name</PArticipantID>
I would expect the following class to work (code shown in kotlin):
data class ParticipantID(
#JacksonXmlProperty(isAttribute = true)
var code:String,
#JacksonXmlText
var value:String
)
yet serializing produces
<ParticipantID> <code>AA</code> participant name</PArticipantID>

It turns out that it is possible to make this work using the following class:
class ParticipantID(code:String, value:String) {
#JacksonXmlProperty(isAttribute = true)
private var code:String = code
get() = field
#JacksonXmlText
private var value:String = value
get() = field
}

Related

Java :: JacksonXmlProperty :: Multiple fields with same name

I'm extending code from an existing Java class that serializes to and from XML. The existing class is somewhat like this:
#Getter
#JacksonXmlRootElement("element")
public class Element {
#JacksonXmlProperty(localName = "type", isAttribute = true)
private String type;
}
The type field has a finite set of possible values so I created an enum Type with all possible values and (to avoid breaking existing functionality) added a new field to the class, like so:
#Getter
#JacksonXmlRootElement("element")
public class Element {
#JacksonXmlProperty(localName = "type", isAttribute = true)
private String type;
#JacksonXmlProperty(localName = "type", isAttribute = true)
#JsonDeserialize(using = TypeDeserializer.class)
private Type typeEnum;
}
This gives me the following error:
Multiple fields representing property "type": Element#type vs Element#typeEnum
I understand why this is a problem cuz when Jackson would try to serialize the class, two fields in my class map onto the same field in the output XML.
I tried adding a #JsonIgnore on one of the fields and it gets rid of the error but has the side effect of not populating the ignored field either. Is there a way to annotate that a field should be deserialized (while reading XML) but not serialized (while writing XML)?
I really need to keep both fields in the class to not disturb any legacy code that might be using the first field, but at the same time allow newer code to leverage the second field.
Thank you!

Serialize/Deserialize using JsonTypeInfo

My goal is to convert a JSON string field to the right class using Jackson.
I have the following class:
public class AnimalRecord {
private String id;
private String source;
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "source", include = JsonTypeInfo.As.EXTERNAL_PROPERTY)
#JsonSubTypes(value = {
#JsonSubTypes.Type(value = CatProbeMetadata.class, name
= "catProbeMetadata"),
#JsonSubTypes.Type(value = DogProbeMetadata.class, name = "dogProbeMetadata"),
})
private AnimalMetadata metadata;
In addition to this class, I have a DB table where I store records of AnimalRecord (AnimalRecord = row). The AnimalMetadata is a different JSON string based on the source of this class. Each source has it's own metadata and class definition. In this example, CatProbeMetadata class will be the output when doing de-serialization from the string when the source is "cat".
The issue is that I'm not sure what to do when reading the rows from the DB. I have the following method:
private class ActiveProbeWrapper implements RowMapper<ActiveProbeRecord> {
#Override
public ActiveProbeRecord mapRow(ResultSet rs, int rowNum) throws SQLException {
String id= rs.getString("id");
String source= rs.getString("source");
Animalmetadata metadata = // NOT SURE WHAT TO DO HERE;
ActiveProbeRecord record = new ActiveProbeRecord(deviceId,segment, source, metadata);
return record;
}
}
I need to convert the string in the DB to the right class instance, but my metadata string will NOT include the source (since it's outside the metadata JSON).
The question: Will I have to add the "source" field to the metadata itself or is there any better way of doing this that I missed?
Updated example:
Example of DB rows:
id | source | metadata
1 | catSource | {"catName": "Mewy"}
2 | dogSource | {"dogName": "Barky"}
When I read the rows from the DB I want to use the source field to de-serialize metadata to the right class - String --> CatMetadata
Jackson 2.12 introduced a
new feature for type deduction :
#JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION)
#JsonSubTypes({
#JsonSubTypes.Type(DogMetadata.class),
#JsonSubTypes.Type(CatMetadata.class) })
public abstract class AnimalMetadata {
}
and therefore:
AnimalMetadata metadata = om.readValue("{\"catName\": \"Paws\"}", AnimalMetadata.class);
assertThat(metadata).isInstanceOf(CatMetadata.class);
The downside is that it might break if Jackson can't figure out which subtype to use based solely on properties names.
With this solution, optional json fields (like an absent catName property), or too similar subtypes may rise problems. #Sergei solutions doesn't have these issues (also, his solution makes use of the source field, which was your requirement).
On a side note, if you're working with SpringBoot, upgrading jackson is a matter of adding this property in pom.xml
<jackson-bom.version>2.12.3</jackson-bom.version>
The property attribute of the #JsonTypeInfo annotation marks the property that defines the entity subclass, and include = JsonTypeInfo.As.EXTERNAL_PROPERTY means that this property should be included not inside the metadata value, but on an upper level, as a property of the AnimalRecord class. This will only work if you parse the string as the AnimalRecord class.
This property should contain the value catProbeMetadata for cats and dogProbeMetadata of dogs, otherwise Jackson won't know how to parse the contents of your source field. The property may be also included inside the source string itself, but then you have to use include = JsonTypeInfo.As.PROPERTY.
Approach 1 - type is inside the metadata
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = CatProbeMetadata.class, name = "catProbeMetadata"),
#JsonSubTypes.Type(value = DogProbeMetadata.class, name = "dogProbeMetadata"),
})
class AnimalMetadata {
private String type;
}
class CatProbeMetadata extends AnimalMetadata {
private String catName;
}
class DogProbeMetadata extends AnimalMetadata {
private String dogName;
}
class AnimalRecord {
private AnimalMetadata metadata;
}
Then you could parse it as follows:
ObjectMapper mapper = new ObjectMapper();
AnimalRecord catRecord = new AnimalRecord();
catRecord.setMetadata(mapper.readValue("{\"type\":\"catProbeMetadata\",\"catName\": \"Paws\"}", AnimalMetadata.class));
AnimalRecord dogRecord = new AnimalRecord();
dogRecord.setMetadata(mapper.readValue("{\"type\":\"dogProbeMetadata\",\"dogName\": \"Fido\"}", AnimalMetadata.class));
Approach 2 - type is outside of metadata
Just select the class manually, based on the type. You don't need any annotations:
class AnimalMetadata {
}
class CatProbeMetadata extends AnimalMetadata {
private String catName;
}
class DogProbeMetadata extends AnimalMetadata {
private String dogName;
}
class AnimalRecord {
private String type;
private AnimalMetadata metadata;
}
Then you can parse like this. Putting the selection logic inside a separate method has exactly same consequences as putting it into annotations - you just need to update a different piece of code if you want to add a new subclass:
public Class<? extends AnimalMetadata> getMetadataClass(AnimalRecord record) {
switch (record.getType()) {
case "cat":
return CatProbeMetadata.class;
case "dog":
return DogProbeMetadata.class;
default:
throw new UnsupportedOperationException();
}
}
public void parse() {
ObjectMapper mapper = new ObjectMapper();
AnimalRecord catRecord = new AnimalRecord();
catRecord.setType("cat");
catRecord.setMetadata(mapper.readValue("{\"catName\": \"Paws\"}", getMetadataClass(catRecord)));
AnimalRecord dogRecord = new AnimalRecord();
dogRecord.setType("dog");
dogRecord.setMetadata(mapper.readValue("{\"dogName\": \"Fido\"}", getMetadataClass(dogRecord)));
}

Elements annotated with #XmlElementRef not showing up in JAXB Marshalled Strings?

The Background
I am working with an application that uses data structures that we have retrieved and generated using jaxws-maven-plugin to communicate with an external application. We are able to receive the XML documents and unmarshall them into java objects using JAXB and the class structures they have provided us. However, when we re-marshall these objects into Strings to persist in the database for future debugging, we lose information.
A snippet of the objects we use to convert:
/*the "root" object*/
//XML tags indicating field access, name, and prop order
public class PersonAppResObj {
#XmlElement(name = "ApplicationObj", required = true)
protected ApplicationObj application;
#XmlElement(name = "ContactInfo", required = true)
protected List<ContactInfo> contactInfo;
...
}
//XML tags indicating field access, name, and prop order
public class ApplicationObj extends other.pkg.ApplicationObj {
#XmlElement(name = "PrimaryApplicant")
protected List<XObj> primaryApplicant;
...
}
package other.pkg
//XML tags indicating field access, name, and prop order
public class ApplicationObj extends GenericApp {
#XmlElementRef(name="Applicant", namespace="http://pkg.com/1/pkgs", type=JAXBElement.class, required=false)
protected List<JAXBElement<? extends Applicant>> applicant;
#XmlElement(name = "ApplicationPrimaryContact", required = true)
protected XObj applicationPrimaryContact;
...
}
The following snippets are a simple Kotlin object that converts between the JAXB elements and the Strings, and a test for that object:
object Converter {
fun xmlToXObj(respString: String): XObj? {
val jaxbContext = JAXBContext.newInstance(ResponseRoot().javaClass)
val unmarshaller = jaxbContext.createUnmarshaller()
unmarshaller.setEventHandler {
println(it.message)
true
}
return (unmarshaller.unmarshal(ByteArrayInputStream(respString.toByteArray())) as ResponseRoot).xObj
}
fun xobjToXML(resp: XObj): String {
val responseRoot = ResponseRoot(); responseRoot.xobj = resp
val resMarsh = JAXBContext.newInstance(responseRoot.javaClass).createMarshaller()
val resStream = ByteArrayOutputStream()
resMarsh.marshal(responseRoot, resStream)
return String(resStream.toByteArray())
}
}
//autowired var feClient fetches info from other application
#Test
fun readWrite() {
val rawResponse = feClient.fetchApp("AA", "1A")
val xmlString = Converter.xobjToXML(rawResponse)
val parsedResponse = Converter.xmlToXObj(xmlString)
val parsedTwiceXML = Converter.xobjToXML(parsedResponse!!)
println(xmlString)
println(parsedTwiceXML)
}
now, to the real bulk of the issue:
When I run the readWrite test, I lose the Applicant information from the other.pkg.ApplicationObj:
//println(xmlString)
<ns4:PersonAppResObj>
<ns4:ApplicationObj>
<ns4:Applicant ns1:id="Applicant1">
...
<ns3:PrimaryContact ns1:ref="primaryContact"/>
//println(parsedTwiceXML)
<ns4:PersonAppResObj>
<ns4:ApplicationObj>
<ns3:PrimaryContact ns1:ref="primaryContact"/>
//no applicant to be found
The error that comes up in console is
unexpected element (uri:"...", local:"Applicant"). Expected elements are <{...}PrimaryFilerPerson>,<{...}JointFilerPerson>,<{...}ApplicationPrimaryContact>,<{...}ApplicationMultipleIndicator>,<{...}Applicant>
The Main Issue
There were two similar errors that came up in the console, and those elements were missing as well. When I looked at those objects in the generated class files, all 3 elements that didn't show up were annotated with #XmlElementRef.
Firstly, what could be causing this issue? Is it just a case of the marshaller not finding the referenced objects?
Second, is there a way to fix this issue without editing the generated objects at all? If not, what can I do without changing how the xml will look?

Maintaining ordering of Class fields during Json Deserialization

Lets say I have a class as follows:
#JsonIgnoreProperties(ignoreUnknown = true)
class MyClass {
#JsonProperty(value="vertical")
private String vertical;
private String groupId;
#JsonProperty(value = "relationships")
private void unwrapGroupId(Map<String, Map<String, List<Map<String, Object>>>> relationships) {
this.groupId = ""; // Some logic to process the relationships map & set this.groupId based on the value set in this.vertical during deserialization
}
}
When deserializing an API Response to MyClass, is it guaranteed that vertical field is set before unwrapGroupId() is processed???? Else my processing in unwrapGroupId() would fail as this.vertical will be empty. If not , how can it be achieved.
I looked up #JsonPropertyOrder, but looks like it doesnt solve this usecase
Note: I use Jackson 2.8.1

Nesting multiple levels of Jackson WRAPPER_OBJECTs

By no means am I a Jackon/JSON wizard, which is probably evident from the following issue I'm running into:
I have 2 possible data structures I'm receiving.
The first one is called amountTransaction:
{
"amountTransaction": {
"clientCorrelator":"54321",
"endUserId":"tel:+16309700001"
}
}
Which is represented by the following Java object:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
#JsonTypeName(value = "amountTransaction")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class AmountTransaction {
private String clientCorrelator;
private String endUserId;
...
}
However the amountTransaction object also appears as child element of the paymentTransactionNotification object:
{
"paymentTransactionNotification": {
"amountTransaction": {
"clientCorrelator": "54321",
"endUserId": "tel:+16309700001"
}
}
}
..which I thought would be represented by:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
#JsonTypeName(value = "paymentTransactionNotification")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class PaymentTransactionNotification {
private AmountTransaction amountTransaction;
...
}
Parsing the JSON with the amountTransaction object alone works fine. It's a pretty straightforward example of a WRAPPER_OBJECT.
However when trying to parse the JSON for the paymentTransactionNotification, I'm getting an exception indicating that it can't properly deal with the amountTransaction as element of the paymentTransactionNotification:
com.fasterxml.jackson.databind.JsonMappingException: Could not resolve type id 'clientCorrelator' into a subtype of [simple type, class com.sf.oneapi.pojos.AmountTransaction]
Any thoughts on how I can properly annotate this so my code can properly deal with both stand alone, as well as encapsulated amountTransaction objects?
By default wrapping root node in Jackson is disabled. You can wrap inner objects but if you want to wrap root node you need to enable jackson feature for it (https://jira.codehaus.org/browse/JACKSON-747):
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationConfig.Feature.WRAP_ROOT_VALUE);
objectMapper.enable(DeserializationConfig.Feature.UNWRAP_ROOT_VALUE);
When you enabled these features you already said Jackson to wrap the root element and you don't need #JsonTypeInfo and #JsonTypeName anymore. You can simple delete them. But now you need to customize the root node name and you can use #JsonRootName for it. Your classes should look like this:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonRootName("amountTransaction")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class AmountTransaction {
private String clientCorrelator;
private String endUserId;
...............
}
And
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonRootName("paymentTransactionNotification")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class PaymentTransactionNotification {
private AmountTransaction amountTransaction;
.............
}
I've tried and Jackson converted both JSON requests as expected.

Categories

Resources