How to Define Multiple Names for Xmlelement Field

How to define multiple names for XmlElement field?

Take 2 - let's implement this ourselves using the unknown element handling event (see the comments below for some limitations though):

public class XmlSynonymDeserializer : XmlSerializer
{
public class SynonymsAttribute : Attribute
{
public readonly ISet<string> Names;

public SynonymsAttribute(params string[] names)
{
this.Names = new HashSet<string>(names);
}

public static MemberInfo GetMember(object obj, string name)
{
Type type = obj.GetType();

var result = type.GetProperty(name);
if (result != null)
return result;

foreach (MemberInfo member in type.GetProperties().Cast<MemberInfo>().Union(type.GetFields()))
foreach (var attr in member.GetCustomAttributes(typeof(SynonymsAttribute), true))
if (attr is SynonymsAttribute && ((SynonymsAttribute)attr).Names.Contains(name))
return member;

return null;
}
}

public XmlSynonymDeserializer(Type type)
: base(type)
{
this.UnknownElement += this.SynonymHandler;
}

public XmlSynonymDeserializer(Type type, XmlRootAttribute root)
: base(type, root)
{
this.UnknownElement += this.SynonymHandler;
}

protected void SynonymHandler(object sender, XmlElementEventArgs e)
{
var member = SynonymsAttribute.GetMember(e.ObjectBeingDeserialized, e.Element.Name);
Type memberType;

if (member != null && member is FieldInfo)
memberType = ((FieldInfo)member).FieldType;
else if (member != null && member is PropertyInfo)
memberType = ((PropertyInfo)member).PropertyType;
else
return;

if (member != null)
{
object value;
XmlSynonymDeserializer serializer = new XmlSynonymDeserializer(memberType, new XmlRootAttribute(e.Element.Name));
using (System.IO.StringReader reader = new System.IO.StringReader(e.Element.OuterXml))
value = serializer.Deserialize(reader);

if (member is FieldInfo)
((FieldInfo)member).SetValue(e.ObjectBeingDeserialized, value);
else if (member is PropertyInfo)
((PropertyInfo)member).SetValue(e.ObjectBeingDeserialized, value);
}
}
}

And now the actual code of the class would be:

[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
[XmlSynonymDeserializer.Synonyms("LeParentId", "AnotherGreatName")]
public long ParentId { get; set; }
//rest of fields...
}

To deserialize, simply use XmlSynonymDeserializer instead of the regular XmlSerializer. This should work for most of the basic needs.

Known limitations:

  • This implementation supports only elements with multiple names; extending it for attributes should be trivial
  • Support for handling of properties/fields in cases where the entities inherit from one another is not tested
  • This implementation does not check for programming bugs (having the attribute on read-only/constant field/properties, multiple members with the same synonyms and so on)

@XmlElement with multiple names

Note:

The answer given by Ilya works but isn't guaranteed to work across all implementations of JAXB or even across versions of a single JAXB implementation. The @XmlElements annotation is useful when the decision of which element to marshal depends on the type of the value (see: http://blog.bdoughan.com/2010/10/jaxb-and-xsd-choice-xmlelements.html). In your use case both the ResponseCode and ResultCode elements correspond to type String, unmarshalling will always work fine, but the choice of which element to output is arbitrary. Some JAXB Impls may have last specified wins, but others could easily have first wins.


You could do the following by leveraging @XmlElementRef.

Java Model

ResponseAPI

We will change the responseCode property from type String to JAXBElement<String>. The JAXBElement allows us to store the element name as well as the value.

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.*;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class ResponseAPI{

@XmlElementRefs({
@XmlElementRef(name = "ResponseCode"),
@XmlElementRef(name = "ResultCode")
})
private JAXBElement<String> responseCode;

public JAXBElement<String> getResponseCode() {
return responseCode;
}

public void setResponseCode(JAXBElement<String> responseCode) {
this.responseCode = responseCode;
}

}

ObjectFactory

The @XmlElementRef annotations we used on the ResponseAPI class correspond to @XmlElementDecl annotations on a class annotated with @XmlRegistry. Traditionally this class is called ObjectFactory but you can call it anything you want.

import javax.xml.bind.JAXBElement;
import javax.xml.bind.annotation.*;
import javax.xml.namespace.QName;

@XmlRegistry
public class ObjectFactory {

@XmlElementDecl(name="ResponseCode")
public JAXBElement<String> createResponseCode(String string) {
return new JAXBElement<String>(new QName("ResponseCode"), String.class, string);
}

@XmlElementDecl(name="ResultCode")
public JAXBElement<String> createResultCode(String string) {
return new JAXBElement<String>(new QName("ResultCode"), String.class, string);
}

}

Demo Code

input.xml

<responseAPI>
<ResponseCode>ABC</ResponseCode>
</responseAPI>

Demo

When creating the JAXBContext we need to ensure that we include the class that contains the @XmlElementDecl annotations.

import java.io.File;
import javax.xml.bind.*;

public class Demo {

public static void main(String[] args) throws Exception {
JAXBContext jc = JAXBContext.newInstance(ResponseAPI.class, ObjectFactory.class);

Unmarshaller unmarshaller = jc.createUnmarshaller();
File xml = new File("Scratch/src2/forum24554789/input.xml");
ResponseAPI responseAPI = (ResponseAPI) unmarshaller.unmarshal(xml);

ObjectFactory objectFactory = new ObjectFactory();
String responseCode = responseAPI.getResponseCode().getValue();
JAXBElement<String> resultCodeJAXBElement = objectFactory.createResultCode(responseCode);
responseAPI.setResponseCode(resultCodeJAXBElement);

Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(responseAPI, System.out);
}

}

Output

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<responseAPI>
<ResultCode>ABC</ResultCode>
</responseAPI>

JAXB - XmlElement with multiple names and types

I got it to work using the @XmlElements annotation, as follows:

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;

public class Main {
public static void main(String[] args) throws JAXBException {
String xml = "<owner><dog></dog></owner>";
JAXBContext jaxbContext = JAXBContext.newInstance(Owner.class);
Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
Owner owner = (Owner) jaxbUnmarshaller.unmarshal(
new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));

System.out.println(owner.getAnimal().getClass());
}
}

abstract class Animal {}

class Dog extends Animal {}

class Cat extends Animal {}

class Lion extends Animal {}

@XmlRootElement
class Owner {
@XmlElements({
@XmlElement(name = "dog", type = Dog.class),
@XmlElement(name = "cat", type = Cat.class),
@XmlElement(name = "lion", type = Lion.class)
})
private Animal animal;

public Animal getAnimal() {
return animal;
}
}

Using the default JAXB implementation that ships with the Oracle Java 8 SDK, this prints out:

class Dog

XmlArray with multiple names

Given that the answer from How to define multiple names for XmlElement field? works primarily for elements not arrays, the easiest solution would seem to be to introduce a surrogate property for one of the image elements, say <smallImages>:

public class root 
{
[XmlArray("largeImages")]
[XmlArrayItem("largeImage")]
public List<image> Images { get; set; } = new List<image>();

[XmlArray("smallImages")]
[XmlArrayItem("smallImage")]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
public List<image> SmallImagesSurrogate { get { return Images; } }

public bool ShouldSerializeSmallImagesSurrogate() { return false; }
}

Notes:

  • When deserializing to a pre-allocated List<T>, XmlSerializer will simply append deserialized elements to the existing list. That allows elements from both <largeImages> and <smallImages> to be concatenated into the same collection without data loss.

  • The surrogate property must be public but can be made "less visible" by setting attributes such as [Browsable(false)].

  • XmlSerializer can successfully deserialize get-only collection properties as long as the collection is pre-allocated. I took advantage of this to avoid adding a setter for the surrogate.

  • In case you need to re-serialize your root, the method ShouldSerializeSmallImagesSurrogate() will prevent the images array from being double-serialized. (For an explanation of why, see ShouldSerialize*() vs *Specified Conditional Serialization Pattern.) Instead all the images will serialize under <largeImages>. This method does not affect deserialization.

Sample working .Net fiddle.

C# XmlDeserializer / XmlElement different names

You can use the [XmlChoiceIdentifier] attribute to support the different variations of the XML file. It's a bit involved but here's a sample implementation for your use. Basically, we're going to be adding an enum for the different element names, and add an additional property to your partTerminal to flag which specific name to use. (but it won't be visible in the XML)

First, your partTerminal will look like this:

public partial class partTerminal
{
[XmlChoiceIdentifier("TerminalType")]
[XmlElement("round")]
[XmlElement("hf")]
[XmlElement("box")]
public partTerminalType Type { get; set; }

[XmlIgnore]
public TerminalChoiceType TerminalType { get; set; }
}

Notice the addition of TerminalType and the [XmlChoiceIdentifier]. The use of [XmlIgnore] on TerminalType makes sure that it won't be included in your XML files.

We also need to add the TerminalChoiceType enum as follows:

public enum TerminalChoiceType
{
round,
hf,
box
}

You can now serialize/deserialize it as normal. Note that you can discern what kind of tag was used in the XML file as TerminalType will be set automtically to the corresponding value. Vice versa, when serializing, if you want it to write as a box value, simply set TerminalType = TerminalChoiceType.box and the serializer will honour that.

Multiple elements with the same type but different name?

You could try making a class that stores the fields in them, and then serialize that class.

@XmlRootElemnt(name="root")
Class ContactInformation{

private String name;

@XmlElement(name="Name")
public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}
}

I'm used to using Gson.toJson(), so I'm not much help on serializing to XML; hopefully this points you in the right direction though.

Add same XmlElement of Name with other type

Use this:

[XmlType("Phone")]
public class Phone
{
[XmlAttribute("type")]
public string Type { get; set; }
[XmlText]
public string Value { get; set; }
}

[XmlType("Employee")]
public class Employee
{
[XmlElement("EmpId", Order = 1)]
public int Id { get; set; }

[XmlElement("Name", Order = 2)]
public string Name { get; set; }

[XmlElement(ElementName = "Phone", Order = 3)]
public Phone phone_home { get; set; }

[XmlElement(ElementName = "Phone", Order = 4)]
public Phone phone_work { get; set; }

public Employee() { }
public Employee(string home, string work)
{
phone_home = new Phone()
{
Type = "home",
Value = home
};
phone_work = new Phone()
{
Type = "work",
Value = work
};
}

public static List<Employee> SampleData()
{
return new List<Employee>()
{
new Employee("h1","w1"){
Id = 1,
Name = "pierwszy",
},
new Employee("h2","w2"){
Id = 2,
Name = "drugi",
}
};
}
}

Serialize code:

var employees = Employee.SampleData();

System.Xml.Serialization.XmlSerializer x =
new System.Xml.Serialization.XmlSerializer(employees.GetType());

x.Serialize(Console.Out, employees);

Here is your result:

<?xml version="1.0" encoding="windows-1250"?>
<ArrayOfEmployee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Employee>
<EmpId>1</EmpId>
<Name>pierwszy</Name>
<Phone type="home">h1</Phone>
<Phone type="work">w1</Phone>
</Employee>
<Employee>
<EmpId>2</EmpId>
<Name>drugi</Name>
<Phone type="home">h2</Phone>
<Phone type="work">w2</Phone>
</Employee>
</ArrayOfEmployee>


Related Topics



Leave a reply



Submit