FasterXML / jackson-dataformat-xml

Extension for Jackson JSON processor that adds support for serializing POJOs as XML (and deserializing from XML) as an alternative to JSON
Apache License 2.0
573 stars 222 forks source link

Support for providing namespace to use for fields of a Class (unless overridden by field) #18

Open mortenoh opened 12 years ago

mortenoh commented 12 years ago

Currently in the xml-module you have to qualify each field with the wanted namespace. This in many ways are similar to JAXB, but JAXB provides @XmlSchema for adding namespaces at the package level.

It would be nice if one could provide a default namespace for a class, so that theres not need to qualify every single field.

Even better would be to provide this at the package level, but class level should be more than ok for now.

cowtowncoder commented 12 years ago

Jackson does not support per-package annotations (except for specific case of JAXB annotations), so that'd be sort of new feature altogether (and this time, for jackson-databind :) ), but let's start with per-class one. Class annotations are inherited, so it can work as nicely if you have shared base class or interface(s)

Poorman65 commented 9 years ago

I am having a major problem with this limitation. I didn't realize that @XmlSchema was not supported with the XML annotation support. Once I added a namespace to my root element with @XmlRootElement or @JacksonXmlRootElement, the namespace shows up in the root properly, but every child element has xmlns="" added to it. It will be very difficult to maintain if we have to add namespacing to every field in our model and would be prone to errors over time.

Below is sample output based on code from a bug that was fixed in 2012. When the bug was first fixed the output looked fine but the xmlns="" started showing up in 2.0.3. The output is with version 2.4.3 and with woodstox-core-asl version 4.4.1

<person xmlns="http://example.org/person">
  <name xmlns="">Name</name>
  <age xmlns="">30</age>
  <notes xmlns="">
    <note>This is note #1</note>
    <note>This is note #2</note>
  </notes>
</person>

Is there another way to get the expected results without having to annotate every field?

cowtowncoder commented 9 years ago

@Poorman65 Support for (parts of?) @XmlSchema could be added quite easily, esp. for pre-binding namespace prefixes. At least for use with, say, root element, where it matters most. Or actually, I guess it should just work for base classes; so even if no support exists for package-level annotations (which I am still not sure about -- it does not fit well with Jackson's annotation handling, unfortunately), you could add it to base class/interface, if one exists.

Also: I think there may be another RFE for adding a way to pre-bind namespace prefixes outside of annotations. This would probably make sense as another way to tackle this problem.

For what it is worth, I think addition of xmlns="" was a fix, since if element is to have default namespace (one with URI of ""), it must have that declaration; otherwise it would be within namespace of its parent. In this case it causes issues because element is intended to be in the same namespace, but Jackson does not know that part. So, in a way, two bugs were sort of cancelling each other out.

Poorman65 commented 9 years ago

In my situation I have four classes that can be written as separate files or included inline with a parent (which would be one of the four). Each of these four is in its own package and uses its own namespace.

What I need to do is to have all the children use the parents namespace unless it is one of those four, in which case its branch of the object graph would use that namespace.

If I wanted to override the implemented behavior for my scenario, what class would I need to override? Perhaps the writer?

As for the default namespace fix that is implemented, I don't think I've ever seen this anywhere. It seems that it is implemented as no namespace instead of default namespace. In my example above, the default namespace is xmlns="http://example.org/person" not xmlns="".

cowtowncoder commented 9 years ago

Ok; so the way to customize handling of namespace to use is easiest done by sub-classing XmlAnnotationIntrospector implementation (JacksonXmlAnnotationIntrospector), specifically method "findNamespace()". AnnotationIntrospector handles details of inheritance, so all it needs to do is to find annotation(s) in question, extract namespace info (if any). If none found null is to be returned, as "" is a valid namespace ("default" namespace). One additional complexity here is just that relationship between JAXB annotations module, and XML format module is bit problematic, and handling is split. It is not a problem for your custom sub-class however.

Now: as to fix, empy namespace; there are two meanings to default namespace:

  1. Active binding of the case where no prefix is given
  2. Namespace with URI of empty String

What I meant was, specifically, that in order for a given element to have empty namespace, it is necessary to add xmlns="" declaration, iff active namespace has been bound to some other URI. So the problem in your case is not that such declaration is erroneously added, but that code incorrectly assumed that element should bind to the empty namespace, which is not what you are trying to achieve. If it had left declaration out, active namespace for the element would be namespace of its parent element; which happens to be what you want, but was not what code thinks is what you want.

And so the goal is not to try to suppress namespace binding code (which is actually working exactly like it should), but to make sure target namespace is correctly specified.

cowtowncoder commented 9 years ago

Rats. I see @XmlSchema is ONLY applicable to packages. Bummer. I really, really dislike that aspect of JAXB annotations.

But then again, perhaps supporting package-defaulting for this particular case would be ok. I will file an RFE for JAXB module.

cowtowncoder commented 9 years ago

@Poorman65 Looking at @XmlSchema javadocs, I am not sure it is actually applicable here -- it looks like it affected (as name implies) output of XML Schema generated, and NOT XML instance documents. This based on examples included: they all include XML Schema output, not bound xml document used for data-binding.

Do you have examples that suggest that it would actually be applicable instance documents?

Poorman65 commented 9 years ago

Here is the code for an @XmlSchema example:

package-info.java in schemasample package

@XmlAccessorOrder(XmlAccessOrder.UNDEFINED)
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
@XmlSchema(namespace = "my.xmlschema.person", elementFormDefault = XmlNsForm.QUALIFIED)
package schemasample;

import javax.xml.bind.annotation.XmlAccessOrder;
import javax.xml.bind.annotation.XmlAccessorOrder;
import javax.xml.bind.annotation.XmlNsForm;
import javax.xml.bind.annotation.XmlSchema;
import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
package schemasample;

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;

import javax.xml.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "person")
public class Person {
    @XmlAttribute
    private String id;

    @XmlElement
    private String name;

    @XmlElement
    private Integer age;

    @XmlElementWrapper(name = "notes")
    @XmlElement(name = "note")
    private List<String> notes = new ArrayList<String>();

    public Person() {

    }

    public String getName() {
        return name;
    }

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

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public List<String> getNotes() {
        return notes;
    }

    public void setNotes(List<String> notes) {
        this.notes = notes;
    }

}
package schemasample;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import java.io.IOException;

public class WriterWithSchema {
    public static void main(String[] args) throws IOException, JAXBException {
        new WriterWithSchema().writeXML();
    }

    public void writeXML() throws IOException, JAXBException {
        Person person = new Person();
        person.setName("Name");
        person.setAge(30);
        person.getNotes().add("This is note #1");
        person.getNotes().add("This is note #2");

        JAXBContext jaxbContext = JAXBContext.newInstance(Person.class);

        Marshaller marshaller = jaxbContext.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(person, System.out);
    }
}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<person xmlns="my.xmlschema.person">
    <name>Name</name>
    <age>30</age>
    <notes>
        <note>This is note #1</note>
        <note>This is note #2</note>
    </notes>
</person>
Poorman65 commented 9 years ago

Also, I am still trying to work out the implementation for overriding the namespace behavior. I tried just overriding findNamespace in JaxbAnnotationIntrospector but couldn't figure out what to return. The parent doesn't appear to be accessible from that class.

I am trying now to override the changeProperties method in XmlBeanSerializerModifier to be able to add the parent bean descriptor to my CustomJaxbAnnotationIntrospector as the content is processed.

Not sure if there is something I am missing that would make this easier.

Thanks for the help.

cowtowncoder commented 9 years ago

Hmmh. Ok, so JAXB does seem to directly use the namespace. That is something that javadocs didn't quite show -- I would actually have expected it to lead to separate annotations in generated classes. I think I need to google for more documentation on JAXB specs just to confirm that is the specified behavior.

I am fine adding such functionality, but since JAXB is such a complicated spec, want to make sure I follow its intent here.

As findNamespace(), it just returns namespace URI to use. Prefix to use (explicit, or no prefix to change binding of the default element namespace) is arbitrary.

I hope you don't have to use XmlBeanSerializerModifier, although that is one mechanism that would probably work.

cowtowncoder commented 9 years ago

Ok, as usual, Blaise's blog is more useful than JAXB spec or javadoc. :)

http://blog.bdoughan.com/2010/08/jaxb-namespaces.html

So you are right in usage, and I should add support for it. And also see if @XmlType might need a tune up as well.

jlous commented 8 years ago

+1

dsharp1 commented 8 years ago

Can you post the link to the enhancement that you filed on this? I was curious about the status, as I would like to have this feature as well.

cowtowncoder commented 8 years ago

@dsharp1 Not sure what the question is? No work has been done for this issue.

msillence commented 5 years ago

It is possible to use the XmlMapper with a JacksonXmlAnnotationIntrospector to get the same effect as jaxb and XmlSchema with package level namespaces:

        XmlMapper mapper = new XmlMapper();
        mapper.setAnnotationIntrospector(new JacsonNamespaceInstrospector());
        mapper.writeValue(System.out, person);

    public static class JacsonNamespaceInstrospector extends JacksonXmlAnnotationIntrospector
    {
        private static final long serialVersionUID = 1L;

        private String getNameSpace(Class c) {
            XmlSchema pkgann = c.getPackage().getAnnotation(XmlSchema.class);
            return pkgann.namespace();
        }

        @Override
        public PropertyName findRootName(AnnotatedClass ac)
        {
            String namespace = getNameSpace(ac.getAnnotated());
            if (ac.getAnnotated() != null)
                return new PropertyName(ac.getAnnotated().getSimpleName(), namespace);
            else
                return super.findRootName(ac);
        }

        @Override
        public PropertyName findNameForSerialization(Annotated a)
        {
            AnnotatedElement ae = a.getAnnotated();
            if (Field.class.isInstance(ae)) {
                Field f = (Field)ae;
                String namespace =  getNameSpace(f.getDeclaringClass());
                if (namespace != null)
                    return PropertyName.construct(f.getName(), namespace);
            }

            return super.findNameForSerialization(a);
        }

        @Override
        public String findNamespace(Annotated ann)
        {
            AnnotatedElement ae = ann.getAnnotated();
            if (Method.class.isInstance(ae)) {
                Method m = (Method)ae;
                String namespace = getNameSpace(m.getDeclaringClass());
                if (namespace != null)
                    return namespace;

            }
            return super.findNamespace(ann);
        }
    }
rgoers commented 5 years ago

@msillence I tried this and it seems to not have the desired effect. I am simply trying to add xmlns="http://some.schema.com" to the root element. When I added the code above it started complaining about multiple explicit names on an attribute inside the class that would have the namespace declaration.

metalpalo commented 5 years ago

same problem, JacsonNamespaceInstrospector doesnt work for me, my temporary solution:

  1. remove nameSpace from @JacksonXmlRootElement
  2. add necessary field:
    @JacksonXmlProperty(isAttribute=true, localName = "xmlns") private String xmlns;
Sid-Trikha commented 5 years ago

@cowtowncoder , @msillence I wanted to attach my namespace just to the root-element. I used the JacksonXmlAnnotationIntrospector to add namespace but I wanted the namespace prefix too. Ex:

<?xml version='1.0' encoding='UTF-8'?>
<prefix:cin xmlns:**prefix**="http://www.................">
           <pi>PID</pi>
</prefix:cin> 

By using JacksonXmlAnnotationIntrospector I am able to get

<?xml version='1.0' encoding='UTF-8'?>
<prefix:cin xmlns="http://www.................">
           <pi>PID</pi>
</prefix:cin> 

Prefix isn't there in xmlns:prefix=".........." Any idea ?

msugakov-sh commented 3 years ago

@cowtowncoder in the beginning you mentioned

Class annotations are inherited, so it can work as nicely if you have shared base class or interface(s)

I tried creating an interface with @XmlRootElement(namespace = "http://blah") annotation and inheriting it in my classes but the output came out without any xmlns= attributes. Could you suggest where to look to make it work, please?

cowtowncoder commented 3 years ago

@msugakov-sh a full reproduction (class declaration, json content, code called) would be useful; I can have a look.

msugakov commented 3 years ago

Hi @cowtowncoder

Sure, here you can find the repo: https://github.com/msugakov/jackson-schema-test As you notice, the output xml looks like this (formatted by me):

<House number="30">
    <kitchen numberOfWindows="2"/>
    <bathroom hasHotWater="false"/>
    <wstxns1:livingRoom xmlns:wstxns1="http://this-namespace-will-be-present/">
        <atmosphere>Cosy</atmosphere>
    </wstxns1:livingRoom>
</House>

I was expecting House and Kitchen to have xmlns="http://this-namepace-should-be-present-but-it-is-not/" because they implement Facility interface which has this annotation. Interestingly, Bathroom also does not have xmlns="http://this-namepace-it-is-not-present-also/", but that looks like a separate issue. Finally, it is clear how LivingRoom gets xmlns="http://this-namespace-will-be-present/" but I wonder how could I make its nested <atmosphere> inherit the namespace?

Thanks, Mikhail

cowtowncoder commented 3 years ago

@msugakov thank you for adding more detail. Use of Lombok makes it bit trickier to follow in theory, but I think I can explain why Kitchen does not have namespace: XmlRootElement is only used for root values, not for fields (that is, by Jackson; I don't know how JAXB behaves here). I realize I did not mention this earlier.

But I am not sure why House does not use the namespace: that may be a bug, possibly related to incorrect merging of annotations through inheritance. Or possibly since local name is not defined (Jackson might think there is no annotation if only namespace specified).

I hope to have time to look into this more in future, but right now I have bit of a backlog so that may take a while.

cowtowncoder commented 3 years ago

@msugakov I found the issue wrt <House> and will add a fix for that (to be included in 2.12.1); only affects root elements. Will not change other cases since root element name is not considered for properties by Jackson.

jlous commented 3 years ago

I don't know if this would be hard to implement, but from a user perspective, a straightforward and backwards-compatible API for such a feature would be to add an optional "defaultNamespace" to @JacksonXmlRootElement and @JacksonXmlProperty.

It should apply to all descendant fields unless overridden (individually, or by another default further down the tree). It should also apply to the element itself if its "namespace" is unspecified. If an element ends up with the same namespace as its parent, the xml should optimise it away and rely on implicit namespace.

Off-topic: If this happens, maybe throw in namespaceAlias as well? It should be inheritable in a similar fashion

jlous commented 3 years ago

The root problem here is that the core abstractions in java and xml are fundamentally different. In xml the namespaces is a property of the element type, which in turn combines field name and type in the java mapping.

If every element type mapped to its own class, we could do all namespacing on the class level, but we want to use String etc. for several element types, so we also need to control namespace from the owner-side.

Jackson-xml seems to try to make-do with only owner-side namespacing (except for root elements), but this is a poor match for how xml works.

cowtowncoder commented 3 years ago

Yeah Jackson does not really have concept of "inheritable" attributes/properties for nested Object properties. XML does have binding of the default namespace, but that is bit of orthogonal concept (I am familiar with the way that works, including quirk of attributes never using the default namespace). In that sense, additions to root element values would not make much sense, I'm afraid, since anything non-root properties declare would be properly overridden and only root element's namespace was affected.

For what it is worth, Jackson does not really take namespace information into account on deserialization at all: it does try to match things as expected on serialization side (for interoperability), but not so for deserialization.

lejtemxviw commented 2 years ago

To follow up on an earlier response, I had to modify the JacksonNamespaceIntrospector as shown below, to get it to work in my case. However - this solution seems to ignore the XmlElementWrapper annotation :-(. Anyone how that fix would be incorporated into this approach?

In case this might make a different, I am using Jackson 2.13.3 with all of these dependencies: image And when creating the XmlMapper, I am using:

xmlMapper.registerModule(new ParameterNamesModule()).registerModule(new JavaTimeModule());
xmlMapper.registerModule(new XmlSchemaAwareJaxbAnnotationModule());
xmlMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);

The updated introspector I'm testing:

package gov.wi.etf.eta.batch.io;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import javax.xml.bind.annotation.XmlSchema;

import com.fasterxml.jackson.databind.PropertyName;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.dataformat.xml.JacksonXmlAnnotationIntrospector;

class JacksonNamespaceIntrospector extends JacksonXmlAnnotationIntrospector
{
    private static final long serialVersionUID = 1L;

    private String getNameSpace(Class c) {
        XmlSchema pkgann = c.getPackage().getAnnotation(XmlSchema.class);
        return pkgann == null ? null : pkgann.namespace();
    }

    @Override
    public PropertyName findRootName(AnnotatedClass ac)
    {
        String namespace = getNameSpace(ac.getAnnotated());
        PropertyName candidate = super.findRootName(ac);
        if (candidate.getNamespace() != null)
            return candidate;
        else
            return new PropertyName(candidate.getSimpleName(), namespace);
    }

    @Override
    public PropertyName findNameForSerialization(Annotated a)
    {
        AnnotatedElement ae = a.getAnnotated();
        if (Field.class.isInstance(ae)) {
            Field f = (Field)ae;
            String namespace =  getNameSpace(f.getDeclaringClass());
            if (namespace != null)
                return PropertyName.construct(f.getName(), namespace);
        }

        return super.findNameForSerialization(a);
    }

    @Override
    public String findNamespace(MapperConfig<?> config, Annotated ann)
    {
        AnnotatedElement ae = ann.getAnnotated();
        if (Method.class.isInstance(ae)) {
            Method m = (Method)ae;
            String namespace = getNameSpace(m.getDeclaringClass());
            if (namespace != null)
                return namespace;

        }
        return super.findNamespace(config, ann);
    }
}
lejtemxviw commented 2 years ago

It occurred to me that maybe mixing up use of the JaxbAnnotationModule and the JacksonAnnotationIntrospector might be bad idea - so I tried, again, this time just customizing the JaxbAnnotationModule to support my use case. The code I am using now does mostly what I need - except it generates a namespace for attributes, which I don't want - and it looks like this:

xmlMapper.registerModule(new XmlSchemaAwareJaxbAnnotationModule());
public class XmlSchemaAwareJaxbAnnotationModule extends JaxbAnnotationModule { 

    @Override
    public void setupModule(SetupContext context)
    {
        if (_introspector == null) {
            _introspector = new XmlSchemaAwareJaxbAnnotationIntrospector(context.getTypeFactory());
        }
        super.setupModule(context);
    }
}
@Slf4j
public class XmlSchemaAwareJaxbAnnotationIntrospector extends JaxbAnnotationIntrospector {

    public XmlSchemaAwareJaxbAnnotationIntrospector(TypeFactory typeFactory) {
        super(typeFactory);
    }

    @Getter @Setter private boolean supportXmlSchemaAnnotation = true;

    @Override // AnnotationIntrospector.XmlExtensions
    public String findNamespace(MapperConfig<?> config, Annotated ann)
    {
        String ns = super.findNamespace(config, ann);
        if (ns == null && supportXmlSchemaAnnotation) {
            if (ann instanceof AnnotatedClass) {
                ns = getNameSpace(((AnnotatedClass) ann).getAnnotated());
            } else if (ann instanceof AnnotatedField) {
                ns = getNameSpace(((AnnotatedField) ann).getAnnotated().getDeclaringClass());
            } else {
                log.info("Unexpected annotated: {}", ann.getName());
            }
        }
        return ns;
    }

    @Override
    public PropertyName findWrapperName(Annotated ann)
    {
        PropertyName name = super.findWrapperName(ann);
        if (name != null && supportXmlSchemaAnnotation && !name.hasNamespace()) {
            String ns = getNameSpace(((AnnotatedField) ann).getAnnotated().getDeclaringClass());
            name = new PropertyName(name.getSimpleName(), ns);                                    
        }
        return name;
    }    

    private String getNameSpace(Class c) {
        XmlSchema pkgann = c.getPackage().getAnnotation(XmlSchema.class);
        return pkgann == null ? null : pkgann.namespace();
    }
}