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
572 stars 221 forks source link

Write namespace declaration on root element only? #670

Open carlanton opened 1 month ago

carlanton commented 1 month ago

Hello and thank you for such an awesome project!

I'm using dataformat-xml to create a video manifest (MPEG-DASH mpd), and for aesthetic reasons I'd really like if it could write all namespace declarations on the root element.

The XML I generate currently looks like this:

<MPD xmlns="urn:mpeg:dash:schema:mpd:2011"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd">
  <ContentProtection value="ClearKey1.0" schemeIdUri="urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e">
    <dashif:Laurl xmlns:dashif="https://dashif.org/guidelines/clearKey">https://example.com/license</dashif:Laurl>
  </ContentProtection>
</MPD>

but I want to move the "xmlns:dashif="https://dashif.org/guidelines/clearKey"" to . Like this:

<MPD xmlns="urn:mpeg:dash:schema:mpd:2011"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd"
     xmlns:dashif="https://dashif.org/guidelines/clearKey">
  <ContentProtection value="ClearKey1.0" schemeIdUri="urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e">
    <dashif:Laurl>https://example.com/license</dashif:Laurl>
  </ContentProtection>
</MPD>

Is it possible to configure / override something in XmlMapper or Woodstox to make this work?

Thanks a lot, Anton

Java 21 with com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.2

carlanton commented 1 month ago

Currently I'm doing something like this to deserialize and serialize the video manifest but I get the xmlns:dashif thing on dashif:Laurl.

package io.lindstrom.mpd;

import com.ctc.wstx.api.WriterConfig;
import com.ctc.wstx.stax.WstxInputFactory;
import com.ctc.wstx.stax.WstxOutputFactory;
import com.ctc.wstx.sw.XmlWriter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import org.codehaus.stax2.XMLStreamWriter2;
import org.junit.jupiter.api.Test;

import javax.xml.stream.XMLOutputFactory;

public class GhIssue {

    @JacksonXmlRootElement(localName = "MPD", namespace = "urn:mpeg:dash:schema:mpd:2011")
    private static class MPD {
        @JacksonXmlProperty(isAttribute = true, namespace = "http://www.w3.org/2001/XMLSchema-instance")
        private String schemaLocation;

        @JacksonXmlProperty(localName = "ContentProtection", namespace = "urn:mpeg:dash:schema:mpd:2011")
        private ContentProtection contentProtection;
    }

    private static class ContentProtection {
        @JacksonXmlProperty(isAttribute = true)
        private String value;

        @JacksonXmlProperty(isAttribute = true)
        private String schemeIdUri;

        @JacksonXmlProperty(localName = "Laurl", namespace = "https://dashif.org/guidelines/clearKey")
        private String laurl;
    }

    @Test
    void demo() throws JsonProcessingException {
        String input = """
                <MPD xmlns="urn:mpeg:dash:schema:mpd:2011"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd">
                     <ContentProtection xmlns:dashif="https://dashif.org/guidelines/clearKey"
                         value="ClearKey1.0" schemeIdUri="urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e">
                            <dashif:Laurl>https://example.com/license</dashif:Laurl>
                    </ContentProtection>
                </MPD>
                """;

        XMLOutputFactory xmlOutputFactory = new WstxOutputFactory() {
            @Override
            protected XMLStreamWriter2 createSW(String enc, WriterConfig cfg, XmlWriter xw) {
                XMLStreamWriter2 streamWriter = super.createSW(enc, cfg, xw);
                try {
                    streamWriter.setPrefix("xsi", "http://www.w3.org/2001/XMLSchema-instance");
                    streamWriter.setPrefix("dashif", "https://dashif.org/guidelines/clearKey");
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                return streamWriter;
            }
        };

        ObjectMapper xmlMapper = new XmlMapper(new XmlFactory(new WstxInputFactory(), xmlOutputFactory));
        MPD mpd = xmlMapper.readValue(input, MPD.class);

        String out = xmlMapper.writerWithDefaultPrettyPrinter().writeValueAsString(mpd);
        System.out.println(out);
    }
}
cowtowncoder commented 1 month ago

(note: similar to #666)

So, the short answer is that this might be possible by pre-constructing XMLStreamWriter via Woodstox, and I think there's a way to bind namespace URI/prefix... but not 100% sure there's a way to force writing of namespace except by XMLStreamWriter.writeNamespace(...)... doing which actually would require outputting root element.

Hmmh. So this might not be doable as things are.

But maybe there'd be need for additional handler, to be called right after outputting starting root element -- at which point it's possible to write namespace declarations (as well as possible XML attributes).

I can leave this issue open for adding ability to register such handler.

slusset commented 1 month ago

Even if all elements are in the same namespace is it required to declare namespace on each field? Currently that is what I have to do, e.g.

@JacksonXmlRootElement(namespace = "mynamespace")
public record RxUpdate(

  @JacksonXmlProperty(localName = "EventName", namespace = "mynamespace")
  String EventName,

  @JacksonXmlProperty(localName = "NCPDP", namespace = "mynamespace")
  String NCPDP
cowtowncoder commented 1 month ago

@slusset Yes, there is unfortunately no implied inheritance of namespace information on Java model side, for Jackson.