2

I am working on an API that produces both XML and JSON responses. I have one element in the response which requires an attribute only in XML response. Also, when the value is null, the element shouldn't show up in the response for both formats.

Expectation:
XML:

<name>john</name>
<status type="text">married</status>

JSON:

"name":"john"
"status":"married"

This is my code:

    /**
     * POJO with bunch of LOMBOK annotations to avoid boiler-plate code.
     */
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder(toBuilder = true)
    @Data
    public class User implements Customer, Serializable {

        private static final long serialVersionUID = 1L;

        private Status status;
        private String name;

        /**
         * Matrital status of the user.
         */
        @Builder
        @Value
        public static class Status {

            @JacksonXmlText
            private String maritalStatus;

            @JacksonXmlProperty(isAttribute = true)
            private String type = "text";
        }

    }

With the above change, I am getting the correct XML response but JSON response also returns type=text

 "status" : {
    "maritalStatus" : "married",
    "type" : "text"
  }

I tried to add @JsonValue to private String maritalStatus, that solved the JSON response but it broke XML response by not adding the attribute to the element.

Can someone please help?

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
AMagic
  • 2,690
  • 3
  • 21
  • 33
  • Could you please state that you're using Lombok? Not everybody is familiar with this bunch of annotations. Also, Instead of `@JsonValue` you could try using `@JsonProperty` together with the xml anotation – Marcos Barbero Nov 13 '19 at 21:15
  • Added that I am using Lombok. Thanks for pointing that out. `@JsonProperty` didn't make any difference. – AMagic Nov 13 '19 at 21:38

2 Answers2

0

Probably the easiest way is to implement custom serialiser for User.Status and produce different output for different kinds of representation.

class UserStatusJsonSerializer extends JsonSerializer<User.Status> {

    @Override
    public void serialize(User.Status value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (gen instanceof ToXmlGenerator) {
            ToXmlGenerator toXmlGenerator = (ToXmlGenerator) gen;
            serializeXml(value, toXmlGenerator);
        } else {
            gen.writeString(value.getMaritalStatus());
        }
    }

    private void serializeXml(User.Status value, ToXmlGenerator toXmlGenerator) throws IOException {
        toXmlGenerator.writeStartObject();

        toXmlGenerator.setNextIsAttribute(true);
        toXmlGenerator.writeFieldName("type");
        toXmlGenerator.writeString(value.getType());
        toXmlGenerator.setNextIsAttribute(false);
        toXmlGenerator.writeRaw(value.getMaritalStatus());

        toXmlGenerator.writeEndObject();
    }

    @Override
    public boolean isEmpty(SerializerProvider provider, User.Status value) {
        return value == null || value.getMaritalStatus() == null;
    }
}

Since now, you can remove extra XML annotations and register custom serialiser:

@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true)
@Data
class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Status status;
    private String name;

    @Builder
    @Value
    @JsonSerialize(using = UserStatusJsonSerializer.class)
    public static class Status {
        private String maritalStatus;
        private String type = "text";
    }
}

Simple console app usage could look like below:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Value;

import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;

public class JsonPathApp {

    public static void main(String[] args) throws Exception {
        List<User> users = Arrays.asList(
                createUser("John", "married"),
                createUser("Tom", null));

        ObjectMapper jsonMapper = JsonMapper.builder()
                .enable(SerializationFeature.INDENT_OUTPUT)
                .serializationInclusion(JsonInclude.Include.NON_EMPTY)
                .build();
        for (User user : users) {
            System.out.println(jsonMapper.writeValueAsString(user));
            System.out.println();
        }

        XmlMapper xmlMapper = XmlMapper.builder()
                .enable(SerializationFeature.INDENT_OUTPUT)
                .serializationInclusion(JsonInclude.Include.NON_EMPTY)
                .build();
        for (User user : users) {
            System.out.println(xmlMapper.writeValueAsString(user));
            System.out.println();
        }
    }

    private static User createUser(String name, String maritalStatus) {
        return User.builder()
                .name(name)
                .status(User.Status.builder()
                        .maritalStatus(maritalStatus)
                        .build())
                .build();
    }
}

Above code prints

JSON for John:

{
  "status" : "married",
  "name" : "John"
}

JSON for Tom:

{
  "name" : "Tom"
}

XML for John:

<User>
  <status type="text">married</status>
  <name>John</name>
</User>

XML for Tom

<User>
  <name>Tom</name>
</User>

Notice, that we implemented UserStatusJsonSerializer#isEmpty method which defines what empty means for a Status class. Now, we need to enable JsonInclude.Include.NON_EMPTY feature in your Spring Boot application. Add below key to your application configuration file:

spring.jackson.default-property-inclusion=non_empty

If you do not want to enable inclusion globally you can enable it only for one property using @JsonInclude annotation.

@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Status status;

See also:

Community
  • 1
  • 1
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • Thanks, Michal. I found another way to solve this issue.. But I was going to try your way of using a custom serializer if my "mixin" way would have failed. See my answer. – AMagic Nov 14 '19 at 18:17
0

The solution to marshalling an object one way in XML, but another in JSON (different fields, etc.) was to use "mixins". One trick is that you have to manually register the mixin, there's no magic. See below.

Mixin interface:

public interface UserStatusXmlMixin {

    @JsonValue(false)
    @JacksonXmlText
    String getStatus();

    @JacksonXmlProperty(isAttribute = true)
    String getType();
}

Implementation:

@Value
public class UserStatus implements UserStatusXmlMixin {

    private String status;

    @JsonValue
    @Override
    public String getStatus() {
        return status;
    }

    @Override
    public String getType() {
        return "text";
    }

    /**
     * Returns an unmodifiable UserStatus when status is available, 
     * otherwise return null. This will help to remove this object from the responses.
     */
    public static UserStatus of(final String status) {
        return Optional.ofNullable(status)
                       .map(UserStatus::new)
                       .orElse(null);
    }

}

I also had to register the "mixin" manually.

@Configuration
public class AppJacksonModule extends SimpleModule {

    private static final long serialVersionUID = -1;

    private final Map<Class, Class> mixinByTarget;

    /**
     * Construct an AppJacksonModule.
     */
    public AppJacksonModule() {
        super("AppJacksonModule");
        this.mixinByTarget = Map.of(
                UserStatus.class, UserStatusXmlMixin.class
        );
    }

    @Override
    public void setupModule(final SetupContext context) {
        super.setupModule(context);
        final ObjectCodec contextOwner = context.getOwner();
        if (contextOwner instanceof XmlMapper) {
            mixinByTarget.forEach(context::setMixInAnnotations);
        }
    }

Now wherever I needed to create UserStatus using UserStatus.of(..) if the input param is null, <status/> won't show up in the response.

AMagic
  • 2,690
  • 3
  • 21
  • 33
  • `Optional.ofNullable(status).map(UserStatus::new).orElse(null)`? Perhaps I’m getting old, but how is that better than `status == null ? null : new UserStatus(status)`? – Michael Piefel Nov 16 '19 at 18:24