Home > Enterprise >  Jackson XML :: Trying to write an attribute when there is no open start element
Jackson XML :: Trying to write an attribute when there is no open start element

Time:10-26

I have a class in Java that I want to serialize/deserialize to/from XML. Each property of this class should be serialized/deserialized to/from attributes of the XML element.

The XML looks something like:

<element fullName="Asim" age="30" score="0.78" readonly="true" bounds="[0,0][10,20]" tags="tag1,tag2,tag3">
  ...
  ...
</element>

If the properties are simple (String, int, boolean), this works. I can simply use @JacksonXmlProperty annotation and it gets the job done:

@JacksonXmlProperty(localName = "fullName", isAttribute = true)
private String fullName;

However, some of the properties are class objects (bounds, list) that I need to convert during serialization/deserialization. I have been able to use @JsonDeserialize annotation to read XML:

@JacksonXmlProperty(localName = "bounds", isAttribute = true)
@JsonDeserialize(using = BoundsDeserializer.class)
private Rectangle bounds;

Serializing these field, on the other hand, is proving to be quite a challenge. I've tried using JsonSerialize(using = BoundsSerializer.class) and JsonSerialize(converter = BoundsConverter.class) and nothing works. Either I get the following exception:

com.fasterxml.jackson.core.JsonGenerationException: Trying to write an attribute when there is no open start element.

Or the resulting XML looks like:

<element fullName="Asim" age="30" score="0.78" readonly="true">
  <bounds>
    <x>0</x>
    <y>0</y>
    <width>10</width>
    <height>20</width>
  </bounds>
  <tags tags="tag1" tags="tag2" tags="tag3" />
  ...
  ...
</element>

How can I serialize a non-primitive property of a class as a string attribute in XML?


Edit

The main invocation code:

try {
    XmlMapper mapper = new XmlMapper();
    mapper.enable(SerializationFeature.INDENT_OUTPUT);
    String xml = mapper.writeValueAsString(this);
    return xml;
}
catch (Exception ex) { ... }

Relevant bits of the class to be serialized/deserialized: The commented lines are alternate approaches I tried (and failed).

@JacksonXmlRootElement(localName = "root")
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
@EqualsAndHashCode
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@Slf4j
public class XML {

    @JacksonXmlProperty(localName = "text", isAttribute = true)
    private String text;

    @JacksonXmlProperty(localName = "tags", isAttribute = true)
    @JsonDeserialize(using = ListDeserializer.class)
    @JsonSerialize(converter = ListConverter.class)
    //@JsonSerialize(using = ListSerializer.class)
    //@JsonUnwrapped
    private List<String> tags;

    @JacksonXmlProperty(localName = "bounds", isAttribute = true)
    @JsonDeserialize(using = BoundsDeserializer.class)
    //@JsonSerialize(using = BoundsSerializer.class, contentAs = String.class)
    private Rectangle bounds;

}

Bounds deserializer:

public class BoundsDeserializer extends JsonDeserializer<Rectangle> {

    private static final Pattern BOUNDS_PATTERN = Pattern.compile("\\[(-?\\d ),(-?\\d )]\\[(-?\\d ),(-?\\d )]");

    @Override
    @Nullable
    public Rectangle deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String value = p.getValueAsString();
        if (value.isBlank()) {
            return null;
        }

        Matcher m = BOUNDS_PATTERN.matcher(value);
        if (!m.matches()) {
            return ctxt.reportInputMismatch(Rectangle.class, "Not a valid bounds string: '%s'", value);
        }

        final int x1 = Integer.parseInt(m.group(1));
        final int y1 = Integer.parseInt(m.group(2));
        final int x2 = Integer.parseInt(m.group(3));
        final int y2 = Integer.parseInt(m.group(4));

        return new Rectangle(x1, y1, x2 - x1, y2 - y1);
    }

}

List deserializer:

public class ListDeserializer extends JsonDeserializer<List<String>> {

    @Override
    @Nullable
    public List<String> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String value = p.getValueAsString();
        if (value.isBlank()) {
            return null;
        }

        String deBracketed = value.trim().replaceAll("^\\[(.*)]$", "$1");
        List<String> listValues = Arrays.stream(deBracketed.split(","))
                .map(String::trim)
                .filter(Predicate.not(String::isEmpty))
                .collect(Collectors.toUnmodifiableList());
        return listValues;
    }

}

List converter:

public class ListConverter extends StdConverter<List<String>, String> {

    @Override
    public String convert(List<String> list) {
        return String.join(",", list);
    }

}

Thank you! Asim

CodePudding user response:

So the way I hacked my way around this issue was as follows:

@JacksonXmlProperty(localName = "tags", isAttribute = true)
@JsonDeserialize(using = ListDeserializer.class)
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private List<String> tags;

@JacksonXmlProperty(localName = "tags", isAttribute = true)
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String tagsAsString() {
    return String.join(",", this.tags);
}

The first field is write-only, so it will be deserialized only. The second field is read-only, so it will be serialized only. However, this feels really hackish and I think there has to be a better solution than this. :/

CodePudding user response:

I came up with a better way to do the same thing:

// Used during serialization
@JacksonXmlProperty(localName = "bounds", isAttribute = true)
@JsonSerialize(converter = RectangleToStringConverter.class)

// Used during deserialization
@JsonProperty("bounds")
@JsonDeserialize(converter = StringToRectangleConverter.class)

private Rectangle bounds;
  • Related