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

XML Serialization + JsonAnyGetter as attributes #309

Open
asa-git opened this issue Sep 20, 2018 · 3 comments
Open

XML Serialization + JsonAnyGetter as attributes #309

asa-git opened this issue Sep 20, 2018 · 3 comments
Labels
3.x Issue planned for 3.x (and not 2.x)

Comments

@asa-git
Copy link

asa-git commented Sep 20, 2018

Hi there,

I'm having some issues with the serialization of dynamic data as attributes of an XML element.
Note: Tested on both version 2.9.6 and 2.9.7

Given the Object:

@JacksonXmlRootElement(localName = "metadata")
private static class ObjectWithAttributes {

	@JsonAnyGetter
	public Map<String, String> getData() {
		Map<String, String> map = new HashMap<>();
		map.put("a", "some value");
		map.put("b", "and another one");
		return map;
	}
}

I was expecting to get the following XML :

<metadata a="some value" b="and another one" />

but got:

<metadata>
  <a>some value</a>
  <b>and another one</b>
</metadata>

I tried to fiddle around with some of the annotations available be never got it to serialize the values as attributes.
I even tried to write a custom serializer which worked fine when annotating the class with the @JsonSerialize(using =...) but would results in any other attribute fields to not be serialized :(

I couldn't find any documentations regarding the uses of @JsonAnyGetter to get the values as attributes and not sub elements.

Is this use case not supported by Jackson ?

Thanks in advance,
Best regards.

@cowtowncoder
Copy link
Member

Currently this is not supported, unfortunately. Part of the problem is that "any getter" induced properties are appended after regular ones, and writer assumes/requires that attributes are output first. Handling of attribute/element detection is quite complicated since this concept only exists in XML output: it basically requires reordering properties internally and keeping track of number of properties that are to be output as attributes. This is done, I think, by custom SerializerModifier.

It would be good to think of a way to allow defining that any-properties are to be serialized as attributes (it would be difficult to allow per-property overrides), although I can not yet think of a simple way to make that happen.

@cowtowncoder cowtowncoder added the 3.x Issue planned for 3.x (and not 2.x) label Sep 23, 2018
@asa-git
Copy link
Author

asa-git commented Sep 23, 2018

Thanks for the feedback, I can indeed imaging that this would be a bit of a pain to implement and maintain.

I've ended up writing a Serializer for the whole bean; I just hope it won't grow to much in the future.

For information I did try one other way hoping I could hack it through the use of the @JacksonXmlProperty annotation without specifying a localname, as I assume all the fields annotated with the isAttribute=true would be initially collected to build the root element, but ended up with the exception:

com.fasterxml.jackson.core.JsonGenerationException: Can not write a field name, expecting a value

here is the sample:

public interface MapAsAttributes {
	Map<String, String> getValues();
}

public static class MapSerializer extends StdSerializer<MapAsAttributes> {

	private static final long serialVersionUID = 1L;

	public MapSerializer() {
		super(MapAsAttributes.class, false);
	}

	@Override
	public void serialize(MapAsAttributes obj, JsonGenerator gen, SerializerProvider provider) throws IOException {

		ToXmlGenerator writer = (ToXmlGenerator) gen;

		for (Map.Entry<String, String> entry : obj.getValues().entrySet()) {
			writer.setNextIsAttribute(true);
			writer.writeFieldName(entry.getKey());
			writer.writeRawValue(entry.getValue());
		}
	}
}

@JacksonXmlRootElement(localName = "metadata")
private static class ObjectWithAttributes {

	@JacksonXmlProperty(isAttribute = true)
	public MapAsAttributes getData() {

		Map<String, String> map = new HashMap<>();
		map.put("a", "some value");
		map.put("b", "and another one");
		return () -> map;
	}

	@JacksonXmlProperty(localName = "someelement")
	public String getSomething() {
		return "SomeValue";
	}
}

The serialization was done registering a module with a custom serializer:

final XmlMapper mapper = (XmlMapper) new XmlMapper() //
				.configure(ToXmlGenerator.Feature.WRITE_XML_DECLARATION, true) //
				.enable(SerializationFeature.INDENT_OUTPUT) //
				.registerModule(new SimpleModule().addSerializer(MapAsAttributes.class, new MapSerializer()));

System.out.println(mapper.writeValueAsString(new ObjectWithAttributes()));

@B-R-Bender
Copy link

I'm end up with this implementation.
It's for both JSON and XML.
Hope it'll helps someone.

@JsonSerialize(using = Row.Serializer.class)
public class Row {

    private HashMap<String, String> data;

    public HashMap<String, String> getData() {
        return data;
    }

    public void setData(HashMap<String, String> data) {
        this.data = data;
    }

    public static class Serializer extends StdSerializer<Row> {

        public Serializer() {
            this(null);
        }

        public Serializer(Class<Row> t) {
            super(t);
        }

        @Override
        public void serialize(Row row, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            if (jsonGenerator instanceof ToXmlGenerator) {
                final ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator;
                jsonGenerator.writeStartObject();
                for (Map.Entry<String, String> item : row.getData().entrySet()) {
                    if (item.getValue() == null) {
                        continue;
                    }
                    xmlGenerator.setNextIsAttribute(true);

                    jsonGenerator.writeFieldName(item.getKey());
                    jsonGenerator.writeString(item.getValue());
                }
                jsonGenerator.writeEndObject();
            } else {
                jsonGenerator.writeStartObject();
                for (Map.Entry<String, String> item : row.getData().entrySet()) {
                    if (item.getValue() == null) {
                        continue;
                    }
                    jsonGenerator.writeFieldName(item.getKey());
                    jsonGenerator.writeString(item.getValue());
                }
                jsonGenerator.writeEndObject();
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.x Issue planned for 3.x (and not 2.x)
Projects
None yet
Development

No branches or pull requests

3 participants