Open mortenoh opened 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)
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?
@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.
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="".
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:
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.
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.
@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?
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>
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.
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.
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.
+1
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.
@dsharp1 Not sure what the question is? No work has been done for this issue.
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);
}
}
@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.
same problem, JacsonNamespaceInstrospector doesnt work for me, my temporary solution:
@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 ?
@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?
@msugakov-sh a full reproduction (class declaration, json content, code called) would be useful; I can have a look.
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
@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.
@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.
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
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.
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.
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: 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);
}
}
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();
}
}
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.