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

Type-safe record wrappers #558

Open io7m opened 2 years ago

io7m commented 2 years ago

Is your feature request related to a problem? Please describe.

The following example code uses a trivial wrapper record (LocationID) to allow some extra type-safety when working with elements declared in an existing schema:

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;

import java.io.IOException;
import java.util.Objects;
import java.util.UUID;

public final class SerialDemo2
{
  private SerialDemo2()
  {

  }

  record LocationID(UUID id) {
    LocationID {
      Objects.requireNonNull(id, "id");
    }
  }

  @JacksonXmlRootElement(namespace = "urn:com.io7m.cardant.inventory:1")
  record Location(
    @JsonProperty(required = true)
    @JacksonXmlProperty(isAttribute = true, localName = "id")
    LocationID id,

    @JsonProperty(required = false)
    @JacksonXmlProperty(isAttribute = true, localName = "parent")
    LocationID parent
  ) {
    Location {
      Objects.requireNonNull(id, "id");
    }
  }

  public static void main(
    final String[] args)
    throws IOException
  {
    final var mapper =
      XmlMapper.builder()
        .build();

    System.out.println("Expected: ");
    System.out.println();
    System.out.println("""
<Location xmlns="urn:com.io7m.cardant.inventory:1" 
  id="6e3f4213-db36-4ea3-91ba-1ce6917cbcbb" 
  parent="265f34b3-8c86-4a1f-b23a-bb104238bfc6"/>    
""");

    System.out.println("Received: ");
    System.out.println();
    mapper.writeValue(
      System.out,
      new Location(
        new LocationID(UUID.randomUUID()),
        new LocationID(UUID.randomUUID())
      )
    );
    System.out.println();
  }
}

The output of the program ends up as:

Expected: 

<Location xmlns="urn:com.io7m.cardant.inventory:1"
  id="6e3f4213-db36-4ea3-91ba-1ce6917cbcbb"
  parent="265f34b3-8c86-4a1f-b23a-bb104238bfc6"/>

Received: 

<Location xmlns="urn:com.io7m.cardant.inventory:1"><id xmlns=""><id>c09fb552-e36c-4213-b56a-7729cd5b7999</id></id><parent xmlns=""><id>58c399ae-0256-4dbd-a00e-0c1c01189559</id></parent></Location>

The desired output can be produced with the following definitions instead:

  final class CA1LocationID
  {
    private final UUID id;

    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public CA1LocationID(
      final UUID inId)
    {
      this.id = Objects.requireNonNull(inId, "id");
    }

    @JsonValue
    @Override
    public UUID id()
    {
      return this.id;
    }
  }

But it's slightly unfortunate that the definitions have to be declared in this way, because we lose a lot of the nice properties of record classes in the process.

Describe the solution you'd like

I'd like to be able to use records instead of POJOs in the above example.

Usage example

See above. :slightly_smiling_face:

Additional context

See this thread on the mailing list: https://groups.google.com/d/msgid/jackson-user/20210911172814.62fd2925%40sunflower.int.arc7.info

downloadpizza commented 1 year ago

+1, right now just writing Deserializers, but hoping there will be a better way at some point

cowtowncoder commented 1 year ago

Wrong repo since there's XML-specific parts.

However, I doubt this can be implemented -- problem being that POJOs like LocationID cannot really be included as attributes in general; there is no support for that by default.

Although I guess one thing I would try to add in LocationID would be something like:

// Inside Record definition:

    @JsonValue
    public String serialization() {
       return id.toString()
    }

which would explicitly serialize LocationId as String and then it should be possible to serialize as an XML attribute.

Also if needing deserialization will probably need to specify @JsonCreator(mode = Mode.DELEGATING) to get single-String argument bound properly.