Java - Jackson XML Deserializing by Interface/Abstract class

203 Views Asked by At

I have these 2 XML's that I'm trying to deserialize with jackson-dataformat-xml:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.13.3</version>
</dependency>

These are the 2 XML files:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<query queryname="persons">
    <row>
        <name>charles</name>
        <userid>1</userid>
    </row>
    <row>
        <name>arthur</name>
        <userid>2</userid>
    </row>
</query>
<?xml version="1.0" encoding="ISO-8859-1" ?>
<query queryname="users">
    <row>
        <id>1</id>
        <username>charles</username>
    </row>
    <row>
        <id>2</id>
        <username>arthur</username>
    </row>
</query>

The 2 XMLs display the results of 2 different queries and are differentiated by the queryname attribute of the query element and the row element contains different information. I'm trying to deserialize the 2 XMLs with a single Query class like this:

public interface IRow {
    
}
public class Person implements IRow {
    private String name;
    private String userid;
    
    public String getName() {return name;}
    public String getUserid() {return userid;}
    
    @Override
    public String toString() {
        return "person [name=" + getName() + ", userid=" + getUserid() + "]"; 
    }
}
public class User extends IRow {
    private String id;
    private String username;
    
    public String getId() {return id;}
    public String getUserName() {return username;}
    
    @Override
    public String toString() {
        return "user [id=" + getId() + ", username=" + getUserName() + "]"; 
    }
}
public class Query {
    @JacksonXmlProperty(isAttribute = true)
    private String queryname;
    @JacksonXmlElementWrapper(useWrapping = false)
    private List<IRow> row;
    
    public String getQueryname() {return queryname;}
    public List<IRow> getRow() {return row;}

    @Override
    public String toString() {
        return "query [queryname=" + getQueryname() + ", row=" + getRow() + "]"; 
    }
}
public class Main {
    public static void main(String[] args) throws JsonMappingException, JsonProcessingException {
        XmlMapper xmlMapper = new XmlMapper();
        String sXml = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\r\n"
                + "<query queryname=\"persons\">\r\n"
                + "    <row>\r\n"
                + "        <name>charles</name>\r\n"
                + "        <userid>1</userid>\r\n"
                + "    </row>\r\n"
                + "    <row>\r\n"
                + "        <name>arthur</name>\r\n"
                + "        <userid>2</userid>\r\n"
                + "    </row>\r\n"
                + "</query>";
        Query oQuery = xmlMapper.readValue(sXml,Query.class);
        System.out.println(oQuery);
    }
}

I get the error The main class, rightly, throws the Exception error in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `xmlmanager.jackson.IRow` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

The mistake is right, as in the Query class I defined the row property as a list of IRows. I would need something (I think annotations) that allow me to define and deserialize the row element with the class:

  • Person when attribute queryname = "persons"
  • User when attribute queryname = "users"

How could I do?

Thanks in advance

2

There are 2 best solutions below

0
Charles On BEST ANSWER

The Sascha's solution works even without the JsonTypeInfo and JsonSubTypes annotations and without using the IRow interface:

import java.util.List;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;

public abstract class AbstractQuery<I> {
    @JacksonXmlProperty(isAttribute = true)
    private String queryname;
    @JacksonXmlElementWrapper(useWrapping = false)
    private List<I> row;
    
    public String getQueryname() {return queryname;}
    public List<I> getRow() {return row;}

    @Override
    public String toString() {
        return "query [queryname=" + getQueryname() + ", row=" + getRow() + "]"; 
    }
}
public class Person {
    public static class Query extends AbstractQuery<Person>{}
    
    private String name;
    private String userid;
    
    public String getName() {return name;}
    public String getUserid() {return userid;}
    
    @Override
    public String toString() {
        return "person [name=" + getName() + ", userid=" + getUserid() + "]"; 
    }
}
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;

public class User {
    public static class Query extends AbstractQuery<User> {}
    
    private String id;
    @JacksonXmlProperty(localName = "username")
    private String username;
    
    public String getId() {return id;}
    public String getUserName() {return username;}
    
    @Override
    public String toString() {
        return "user [id=" + getId() + ", username=" + getUserName() + "]"; 
    }
}
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

public class Main {
    public static void main(String[] args) throws JsonMappingException, JsonProcessingException {
        XmlMapper xmlMapper = new XmlMapper();
        String sXmlPersons = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\r\n"
                + "<query queryname=\"persons\">\r\n"
                + "    <row>\r\n"
                + "        <name>charles</name>\r\n"
                + "        <userid>1</userid>\r\n"
                + "    </row>\r\n"
                + "    <row>\r\n"
                + "        <name>arthur</name>\r\n"
                + "        <userid>2</userid>\r\n"
                + "    </row>\r\n"
                + "</query>";
        Person.Query oPersonQuery = xmlMapper.readValue(sXmlPersons,Person.Query.class);
        System.out.println(oPersonQuery);
        
        String sXmlUsers = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\r\n"
                + "<query queryname=\"users\">\r\n"
                + "    <row>\r\n"
                + "        <id>1</id>\r\n"
                + "        <username>charles</username>\r\n"
                + "    </row>\r\n"
                + "    <row>\r\n"
                + "        <id>2</id>\r\n"
                + "        <username>arthur</username>\r\n"
                + "    </row>\r\n"
                + "</query>";
        User.Query oUserQuery = xmlMapper.readValue(sXmlUsers,User.Query.class);
        System.out.println(oUserQuery);
    }
}
4
Sascha On

You will probably need an abstract Query class and a subclass each for Person and User list. Then Jackson can identify your Query subclass by the XML attribute.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME
    , include = JsonTypeInfo.As.PROPERTY
    , property = "queryname"
    )
@JsonSubTypes({
    @JsonSubTypes.Type(name = "users", value = UserQuery.class),
    @JsonSubTypes.Type(name = "persons", value = PersonQuery.class)
})
public abstract class Query<I extends IRow> {
    @JacksonXmlProperty(isAttribute = true)
    private String queryname;
    @JacksonXmlElementWrapper(useWrapping = false)
    private List<I> row;
    
    public String getQueryname() {return queryname;}
    public List<I> getRow() {return row;}
}

@JsonTypeName("users")
public class UserQuery extends Query<User> {}

@JsonTypeName("persons")
public class PersonQuery extends Query<Person>{}