Spring Boot向XML元素添加属性但不在JSON响应中

Posted

技术标签:

【中文标题】Spring Boot向XML元素添加属性但不在JSON响应中【英文标题】:Spring Boot adding attribute to XML element but NOT in JSON response 【发布时间】:2020-03-09 17:55:50 【问题描述】:

我正在开发一个API,它可以同时生成 XML 和 JSON 响应。我在响应中有一个元素,它仅在 XML 响应中需要一个属性。此外,当值为null 时,该元素不应出现在两种格式的响应中。

期望: XML:

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

JSON:

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

这是我的代码:

    /**
     * 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";
        

    

通过上述更改,我得到了正确的 XML 响应,但 JSON 响应也返回 type=text

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

我尝试将@JsonValue 添加到private String maritalStatus,这解决了 JSON 响应,但它通过不向元素添加属性而破坏了 XML 响应。

有人可以帮忙吗?

【问题讨论】:

您能否说明您正在使用 Lombok?不是每个人都熟悉这堆注释。此外,您可以尝试将@JsonProperty 与 xml 注释一起使用,而不是 @JsonValue 补充说我正在使用 Lombok。感谢您指出了这一点。 @JsonProperty 没有任何区别。 【参考方案1】:

在 XML 中以一种方式编组对象但在 JSON(不同字段等)中以另一种方式编组对象的解决方案是使用“mixins”。 一个技巧是你必须手动注册 mixin,没有什么魔法。见下文。

mixin接口:

public interface UserStatusXmlMixin 

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

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

实施:

@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);
    


我还必须手动注册“mixin”。

@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);
        
    

现在,如果输入参数为空,我需要使用 UserStatus.of(..) 创建 UserStatus,&lt;status/&gt; 将不会出现在响应中。

【讨论】:

Optional.ofNullable(status).map(UserStatus::new).orElse(null)?或许我老了,但怎么比status == null ? null : new UserStatus(status)好?【参考方案2】:

可能最简单的方法是为User.Status 实现自定义序列化程序,并为不同类型的表示生成不同的输出。

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;
    

从现在开始,您可以移除多余的XML 注解并注册自定义序列化器:

@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";
    

简单的控制台应用程序用法如下所示:

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();
    

上面的代码打印

John 的 JSON:


  "status" : "married",
  "name" : "John"

Tom 的 JSON:


  "name" : "Tom"

约翰的 XML:

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

汤姆的 XML

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

注意,我们实现了UserStatusJsonSerializer#isEmpty 方法,它定义了emptyStatus 类的含义。现在,我们需要在您的Spring Boot 应用程序中启用JsonInclude.Include.NON_EMPTY 功能。将以下键添加到您的应用程序配置文件中:

spring.jackson.default-property-inclusion=non_empty

如果您不想全局启用包含,则可以使用 @JsonInclude 注释仅对一个属性启用它。

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

另见:

Using Jackson to add XML attributes to manually-built node-tree How to tell Jackson to ignore a field during serialization if its value is null? Spring Boot: Customize the Jackson ObjectMapper

【讨论】:

谢谢,米哈尔。我找到了解决此问题的另一种方法。但是如果我的“mixin”方式失败,我将尝试使用自定义序列化程序的方式。看我的回答。

以上是关于Spring Boot向XML元素添加属性但不在JSON响应中的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot - 添加外部属性文件

如果 xml 元素命名约定与 POJO 属性命名约定不同,则发送到 Spring Boot REST API 的 XML 元素不会映射到 POJO

如果xml元素命名约定与POJO属性命名约定不同,则发送到Spring Boot REST API的XML元素不会映射到POJO

Spring Boot 2.X 实战教程构建代码

如何在 Spring Boot 2 中使用内容协商建立自定义 xml 序列化

Sping Boot入门到实战之入门篇:Spring Boot属性配置