Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MismatchedInputException for XmlIDREF in generic interface attribute #144

Open
raedma opened this issue Jul 4, 2021 · 4 comments
Open

Comments

@raedma
Copy link

raedma commented Jul 4, 2021

I do not know if this is the right location for the issue. If it is more related to handling JAXB annotations, I can move it.

I came across a problem when writing a test that first serializes an object to a string to JSON using Jackson and afterwards, tries to deserialize it. I tried to create a MWE based on the actual problem.

The underlying code uses JAXB annotations with additions for Jackson where necessary. The problem occurs for a generic attribute which itself is an interface. In the MWE there is a class Model with a generic attribute value of type ValueInterface. value itself is used as an @XmlIDREF inside Model. In the underlying tool I use a Memory to store Models and Values independently for reuse, as multiple Model instances may use the same Value object.

What I get is:

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Unexpected token (VALUE_STRING), expected START_OBJECT: need JSON Object to contain As.WRAPPER_OBJECT type information for class org.raedma.jacksonjsondeserializationquestion.ValueInterface

It seems Jackson is unable to resolve the XmlIDREF string, but rather expects the value as a new node. I already tried different solution approaches, e.g. here and here, to tell Jackson that this is a reference to an object having an @XmlID. Unfortunately, nothing helped.

Any ideas?

I should mention that in the underlying code, Value itself is a subtype of some superclasses. Something like @JsonDeserialize(as = MyInterfaceImpl.class) as mentioned here is not possible, otherwise I would not need an interface. The same approach works in JAXB for xml.


MWE

  • Main:

     public class Main{
    
         public static void main(String[] args) throws JsonProcessingException, JAXBException{
             
             Value value = new Value("valueID",1);
             Collection<Value> values = new ArrayList<>();
             values.add(value);
             
             Model<Value> model = new Model<>("modelID", value);
             Collection<Model> models = new ArrayList<>();
             models.add(model);
             
             Memory memory = new Memory();
             memory.setValues(values);
             memory.setModels(models);
             
             //
             json(memory);
             
             //
             //xml(memory);
             
         }
         
         private static void json(Memory memory) throws JsonProcessingException{
             
             // Serialize
             ObjectMapper outObjectMapper = new ObjectMapper();
             outObjectMapper.registerModule(new JaxbAnnotationModule());
             outObjectMapper.enable(SerializationFeature.INDENT_OUTPUT);
             outObjectMapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
             outObjectMapper.setSerializationInclusion(Include.NON_NULL);
    
             DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
             prettyPrinter.indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE);
             outObjectMapper.setDefaultPrettyPrinter(prettyPrinter);
             String result = outObjectMapper.writeValueAsString(memory);
             //System.out.println(result);
             
             // Deserialize
             ObjectMapper objectMapper = new ObjectMapper();
             objectMapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
             objectMapper.registerModule(new JaxbAnnotationModule());
             Memory deserial = objectMapper.readValue(result, Memory.class);
             
         }
         
         private static void xml(Memory memory) throws PropertyException, JAXBException{
             
                 
             // Creating the Document object
             StringWriter document = new StringWriter();
    
             // create JAXB context
             JAXBContext context = JAXBContext.newInstance(memory.getClass());
    
             //Marshaller marshaller = context.createMarshaller();
             Marshaller marshaller = context.createMarshaller();
             marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
             // Marshaling the User object into a Document object
             marshaller.marshal(memory, document);
    
             //
             String result = document.toString();
             
             //System.out.println(result);
            
             // create JAXB context
             JAXBContext context = JAXBContext.newInstance(memory.getClass());
            
            // Create deserialization object
            Unmarshaller unmarshaller = context.createUnmarshaller();
     
            // Create the object
            InputStream stream = new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8));
            Memory deserial = (Memory)unmarshaller.unmarshal(stream);
           
            System.out.println(((Value)deserial.getModels().iterator().next().getValue()).getName());
         }
    
     }
    
  • Value.class:

     @EqualsAndHashCode
    
     @XmlRootElement
     @XmlAccessorType(XmlAccessType.FIELD)
    
     @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
    
     public class Value
             implements
                 ValueInterface<Value>
     {
         
         @XmlID
         @XmlAttribute
         private String name;
         
         private int attribute;
    
         public Value() {}
    
         public Value(
                 String name
                ,int attribute
         ) {
             this.name = name;
             this.attribute = attribute;
         }
    
         public String getName() {return name;}
         public void setName(String name) {this.name = name;}
    
         public int getAttribute() {return attribute;}
         public void setAttribute(int attribute) {this.attribute = attribute;}
    
     }
    
  • ValueInterface:

     @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
     @JsonSubTypes({
         @JsonSubTypes.Type(value = Value.class)
     })
     public interface ValueInterface<
                 T extends ValueInterface<T>
             >
     {
    
     }
    
  • Model:

     @EqualsAndHashCode
    
     @XmlRootElement
     @XmlAccessorType(XmlAccessType.FIELD)
    
     @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
    
     public class Model<
                 V extends ValueInterface
             >
     {
         
         @XmlID
         @XmlAttribute
         private String name;
         
         @XmlIDREF
         @XmlElements({
             @XmlElement(type=Value.class)
         })
         private V value;
         
         public Model() {}
    
         public Model(String name, V value) {
             this.name = name;
             this.value = value;
         }
    
         public String getName() {return name;}
         public void setName(String name) {this.name = name;}
    
         public V getValue() {return value;}
         public void setValue(V value) {this.value = value;}
         
     }
    
  • Memory:

     @EqualsAndHashCode
    
     @XmlRootElement
     @XmlAccessorType(XmlAccessType.FIELD)
    
     @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
    
     public class Memory{
         
         @XmlElementWrapper(name="models")
         @XmlElement(type=Model.class, name="model")
         private Collection<Model> models;
         
         @XmlElementWrapper(name="values")
         @XmlElement(type=Value.class, name="value")
         private Collection<Value> values;
         
         public void setModels(Collection<Model> v){this.models = v;}
         public Collection<Model> getModels(){return models;}
         
         public void setValues(Collection<Value> v){this.values = v;}
         public Collection<Value> getValues(){return values;}
    
     }
    
  • POM.xml:

     <?xml version="1.0" encoding="UTF-8"?>
     <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
         <modelVersion>4.0.0</modelVersion>
         <groupId>org.mr</groupId>
         <artifactId>JacksonJSONDeserializationQuestion</artifactId>
         <version>0.0.1</version>
         <packaging>jar</packaging>
         
         <properties>
             <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
             <maven.compiler.source>13</maven.compiler.source>
             <maven.compiler.target>13</maven.compiler.target>
             <!-- JAXB -->
             <javax.xml.bind.jaxb-api.version>2.4.0-b180830.0359</javax.xml.bind.jaxb-api.version>
             <javax.activation.activation.version>1.1.1</javax.activation.activation.version>
             <org.glassfish.jaxb.jaxb-runtime.version>3.0.2-b01</org.glassfish.jaxb.jaxb-runtime.version>
         </properties>
         
         <dependencies>
             <dependency>
                 <groupId>org.projectlombok</groupId>
                 <artifactId>lombok</artifactId>
                 <version>1.18.18</version>
                 <type>jar</type>
             </dependency>
             <dependency>
                 <groupId>org.junit.jupiter</groupId>
                 <artifactId>junit-jupiter-api</artifactId>
                 <version>5.6.0</version>
                 <scope>test</scope>
             </dependency>
             <dependency>
                 <groupId>org.junit.jupiter</groupId>
                 <artifactId>junit-jupiter-params</artifactId>
                 <version>5.6.0</version>
                 <scope>test</scope>
             </dependency>
             <dependency>
                 <groupId>org.junit.jupiter</groupId>
                 <artifactId>junit-jupiter-engine</artifactId>
                 <version>5.6.0</version>
                 <scope>test</scope>
             </dependency>
             <dependency>
                 <groupId>com.fasterxml.jackson.core</groupId>
                 <artifactId>jackson-databind</artifactId>
                 <version>2.12.3</version>
                 <type>jar</type>
             </dependency>
             <dependency>
                 <groupId>com.fasterxml.jackson.module</groupId>
                 <artifactId>jackson-module-jaxb-annotations</artifactId>
                 <version>2.12.3</version>
                 <type>jar</type>
             </dependency>
             <!-- JAXB -->
             <dependency>
                 <groupId>javax.xml.bind</groupId>
                 <artifactId>jaxb-api</artifactId>
                 <version>2.3.0</version>
             </dependency>
             <dependency>
                 <groupId>com.sun.xml.bind</groupId>
                 <artifactId>jaxb-core</artifactId>
                 <version>2.3.0</version>
             </dependency>
             <dependency>
                 <groupId>com.sun.xml.bind</groupId>
                 <artifactId>jaxb-impl</artifactId>
                 <version>2.3.0</version>
             </dependency>
         </dependencies>
     </project>
    
@cowtowncoder
Copy link
Member

Right, since this is related to JAXB annotation handling, moving to that repo first.

@cowtowncoder cowtowncoder transferred this issue from FasterXML/jackson-databind Jul 4, 2021
@cowtowncoder
Copy link
Member

Ok. So, I am not quite sure of intended semantics with @XmlID and @XmlIDREF here -- they are for Object Identity handling, and for Jackson you'd typically need to indicate use via @JsonIdentityInfo on class that is being referenced.
Combination of Object AND Type Ids gets tricky quite quickly and JAXB semantics for handling differ from those of Jackson.

But it is not 100% clear if you are intending object identities to be retained (this is usually needed to handle cyclic graphs).
Perhaps you do and this is just a simplified case, I just mention it since it seems unnecessary here.

I hope to have time to look into this in near future but do not have immediate ideas of what is causing the issue reading back what Jackson wrote.

@raedma
Copy link
Author

raedma commented Jul 5, 2021

Yes, the purpose is identity handling. I do have a rather complex object structure. With respect of single source of truth, I save each instance of Model and Value only once. Memory fulfills this purpose. Because of that I reference Value as a attribute of Model by its XmlID via XmlIDREF. Reason being that an instance of Value can be used in multiple instances of Model and I do not want the same information multiple times in the serialization of Model.

This is the result of the serialization with Jackson, which looks totally fine to me:

{
  "Memory" : {
    "@type" : "Memory",
    "model" : [
      {
        "@type" : "Model",
        "name" : "modelID",
        "value" : "valueID"
      }
    ],
    "value" : [
      {
        "@type" : "Value",
        "name" : "valueID",
        "attribute" : 1
      }
    ]
  }
}

My goal is to deserialize the JSON string and in return get an instance of Memory with all the proper information. However, Jackson seems to have problems identifying "value" : "valueID" as a reference to the @type: "Value" object.

In case I add @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,property = "name") to Value class, I get the same error as before. Adding it to ValueInterface naturally results in a JsonMappingException as the interface does not know anything about the name ID.

However, adding abstract getters and setters to the interface and than use @JsonIdentityInfo seems to do the trick. So with

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Value.class)
})
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,property = "name")
public interface ValueInterface<
            T extends ValueInterface<T>
        >
{
    
    public abstract void setName(String v);
    public String getName();

}

I get no error and it seems the deserialization works. Is this the way to go?

@cowtowncoder
Copy link
Member

@raedma Yes, that usage looks fine -- I would be slightly worried about combination of generic types, identity (object id) and polymorphism (type id) as generics can sometimes cause problems -- but if it does work, great!

JAXB annotations do work to some degree, but there may be some limitations since JAXB model handles things different from Jackson: and all information from JAXB annotations is essentially translate into model Jackson itself uses. Sometimes things do not translate 100%, or some combination might not work as expected.

So I would recommend using Jackson annotations in this case, due to complexity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants