【问题标题】:Jackson Generics with variable Json response object and Json property具有可变 Json 响应对象和 Json 属性的杰克逊泛型
【发布时间】:2021-12-08 09:46:20
【问题描述】:

根据 VizGhar 的回答“Jackson Generics with variable JsonProperty”,我正在尝试使用 WebClient 来使用 API 响应,其中包含 json 响应中的通用对象:

{
"meta": { 
    "RID": "abc9f-defgh-hj78k-lkm9n",
    "QID": "abc9f-defgh-hj78k-lkm9n" },
"data": { 
        "Inquiry": {
            "multiCurrency": [{"TaxStat": "Y", "TaxAmt": 0}],
            "Type": "Tax",
            "TaxFreq": { 
                "weekDay": 0,
                "startDay": 0 
            },
            "TaxRegion": "Tx" 
        } 
    }
}

“查询”的类型是通用的,即“数据”是通用响应对象的包装,在这种情况下是“查询”,但它可能会改变。

查询.java:

public class Inquiry {
    @JsonProperty("multiCurrency")
    private List<MultiCurrencyInq> multiCurrency;

    @JsonProperty("Type")
    private String Type;

    @JsonProperty("TaxFreq")
    private TaxFreq taxFreq;

    @JsonProperty("TaxRegion")
    private String TaxRegion;

    // Getters Setters Constructors
}

MultiCurrencyInq.java:

public class MultiCurrencyInq {

    @JsonProperty("TaxStat")
    private String TaxStat;

    @JsonProperty("TaxAmt")
    private int TaxAmt;

    // Getters Setters Constructors
}

TaxFreq.java:

public class TaxFreq {

    @JsonProperty("weekDay")
    private int weekDay;

    @JsonProperty("startDay")
    private int startDay;

    // Getters Setters Constructors
}

我的 Response.java 看起来像这样:

public class Response<T>{
    private Meta meta;
    private Data<T> data;
    // Getters Setters Constructors
}

元.java:

public class Meta{
    private String RID;
    private String QID;
    // Getters Setters Constructors
}

Data.java:

public class Data<T> {
    // property name, that will be changed
    @JsonProperty(DataNamingStrategy.DATA_FIELD)
    private T data;
    // Getters Setters Constructors
}

我的控制器:

@RestController
public class InquiryController {

    @Autowired private WebClient webClient;

    @GetMapping("/inquiry") public Response<Inquiry> getInquiryApiResponse() {
        ResponseEntity<String> response = webClient.get()
                .uri("http://my.org.com/clientId/inquiry")
                .retrieve()
                .toEntity(String.class)
                .block();

        ObjectMapper mapper = new ObjectMapper();
        mapper.setPropertyNamingStrategy(new DataNamingStrategy("Inquiry")); 
        JavaType type = mapper.getTypeFactory()
                .constructParametricType(Response.class, Inquiry.class);

        Response<Inquiry> res = mapper.readValue(response.getBody(), type);
        return res;
    }
}

DataNamingStrategy.java:

public class DataNamingStrategy extends PropertyNamingStrategy{

    // used by other classes (this will be default field name that should be changed)
    public static final String DATA_FIELD = "variable:data";
    private String fieldName;

    public DataNamingStrategy(String fieldName) {
        this.fieldName = fieldName;
    }

    // use this to change field name (format "variable":"value") not needed in my case
    @Override
    public String nameForField(MapperConfig<?> config, AnnotatedField field,
            String defaultName) {
        return (defaultName.equals(DATA_FIELD))?
            fieldName :
            super.nameForField(config, field, defaultName);
    }

    // use this to change setter method field name (JSON -> Object with format "variable":{})
    @Override
    public String nameForSetterMethod(MapperConfig<?> config,
            AnnotatedMethod method, String defaultName) {
        return (defaultName.equals(DATA_FIELD))?
            fieldName :
            super.nameForGetterMethod(config, method, defaultName);
    }

    // use this to change getter method field name (Object -> JSON with format "variable":{})
    // should be same as nameForSetterMethod
    @Override
    public String nameForGetterMethod(MapperConfig<?> config,
            AnnotatedMethod method, String defaultName) {
        return nameForSetterMethod(config, method, defaultName);
    }
}

这对我不起作用。没有为 Data.java 中的 @JsonProperty(DataNamingStrategy.DATA_FIELD) 将泛型类型设置为 "Inquiry" 的原因可能是什么

【问题讨论】:

  • 我的两分钱:杰克逊图书馆最近发生了变化。这也可能导致此问题。

标签: java json spring-boot jackson


【解决方案1】:

您可以直接使用 WebClient 和 ParameterizedTypeReference 类进行反序列化。以下是您的工作示例:

来自以下代码的响应(查询和 DummyEntity):

// Inquiry Entity
ThirdPartyAPIResponse(meta=ThirdPartyAPIResponse.Meta(rid=abc9f-defgh-hj78k-lkm9n, qid=abc9f-defgh-hj78k-lkm9n), data=ThirdPartyAPIResponse.Data(data=Inquiry(multiCurrency=[MultiCurrencyInq(taxStat=Y, taxAmt=1)], type=Tax, taxFreq=TaxFreq(weekDay=1, startDay=1), taxRegion=Tx)))

// Dummy Entity
ThirdPartyAPIResponse(meta=ThirdPartyAPIResponse.Meta(rid=abc9f-defgh-hj78k-lkm9n, qid=abc9f-defgh-hj78k-lkm9n), data=ThirdPartyAPIResponse.Data(data=DummyEntity(code=200, message=Hello World)))

查询

package com.stackoverflow.q69665171.entities;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter @Setter @ToString
public class Inquiry {
    
    @JsonProperty("multiCurrency") private List<MultiCurrencyInq> multiCurrency;
    @JsonProperty("Type") private String type;
    @JsonProperty("TaxFreq") private TaxFreq taxFreq;
    @JsonProperty("TaxRegion") private String taxRegion;
    
}

MultiCurrencyInq

package com.stackoverflow.q69665171.entities;

import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter @Setter @ToString
public class MultiCurrencyInq {

    @JsonProperty("TaxStat") private String taxStat;
    @JsonProperty("TaxAmt") private Integer taxAmt;
    
}

TaxFreq

package com.stackoverflow.q69665171.entities;

import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter @Setter @ToString
public class TaxFreq {

    @JsonProperty("weekDay") private Integer weekDay;
    @JsonProperty("startDay") private Integer startDay;
    
}

虚拟实体

package com.stackoverflow.q69665171.entities;

import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter @Setter @ToString
public class DummyEntity {

    @JsonProperty("code") private Integer code;
    @JsonProperty("message") private String message;
    
}

ThirdPartyAPIClient (WebClient Impl)

package com.stackoverflow.q69665171.third_party;

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import com.stackoverflow.q69665171.entities.Inquiry;

@Component
public class ThirdPartyAPIClient {

    private WebClient webClient;

    public ThirdPartyAPIClient() {
        webClient = WebClient.create();
    }
    
    // The port is for testing scopes only, as the mock web server
    // defines a random one
    public ThirdPartyAPIResponse<Inquiry> getInquiryApiResponse(int port) {
        return webClient.get()
            .uri("http://localhost:" + port + "/test")
            .retrieve()
            .bodyToMono(
                new ParameterizedTypeReference<ThirdPartyAPIResponse<Inquiry>>(){}
            ).block();
    }

    public ThirdPartyAPIResponse<DummyEntity> getDummyEntityApiResponse(int port) {
        return webClient.get()
            .uri("http://localhost:" + port + "/test2")
            .retrieve()
            .bodyToMono(
                new ParameterizedTypeReference<ThirdPartyAPIResponse<DummyEntity>>(){}
            ).block();
    }

}

第三方API响应

package com.stackoverflow.q69665171.third_party;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter @Setter @ToString
public class ThirdPartyAPIResponse <T> {

    @JsonProperty("meta") private Meta meta;
    @JsonProperty("data") private Data <T> data;
    
    @Getter @Setter @ToString
    public static class Meta {
        @JsonProperty("RID") private String rid;
        @JsonProperty("QID") private String qid;
    }
    
    @Getter @Setter @ToString
    public static class Data <T> {
        @JsonAlias({"Inquiry","DummyEntity"}) private T data;
    }
    
}

Json 虚拟实体

{
    "meta":{
       "RID":"abc9f-defgh-hj78k-lkm9n",
       "QID":"abc9f-defgh-hj78k-lkm9n"
    },
    "data":{
       "DummyEntity":{
          "code": 200,
          "message": "Hello World"
       }
    }
 }

测试

package com.stackoverflow.q69665171;

import static org.junit.Assert.assertEquals;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

import com.stackoverflow.q69665171.third_party.ThirdPartyAPIClient;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;

@SpringBootTest
class ApplicationTests {

    private static MockWebServer webServer;
    @Autowired private ThirdPartyAPIClient client;
    
    @BeforeAll
    static void setUp() throws IOException {
        webServer = new MockWebServer();
        webServer.start();
    }
    
    @AfterAll
    static void tearDown() throws IOException {
        webServer.shutdown();
    }

    
    @Test
    void should_Retrieve_Inquiry_Response_When_Consumes_Inquiry_API() throws Exception {
        
        final String thirdPartyResponse = readJsonTestResource("response.json");
        //System.out.println(thirdPartyResponse);
        
        // Mock
        webServer.enqueue(new MockResponse().setResponseCode(200)
            .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .setBody(thirdPartyResponse)
        );
        
        System.out.println(client.getInquiryApiResponse(webServer.getPort()));
        
        assertEquals(0, 0);
        
    }

    private String readJsonTestResource(String fileName) throws Exception {
        File resource = new ClassPathResource(fileName).getFile();
        return new String(Files.readAllBytes(resource.toPath()));
    }

    

    @Test
    void should_Retrieve_Dummy_Entity_Response_When_Consumes_Dummy_API() throws Exception {
        
        final String thirdPartyResponse = readJsonTestResource("response2.json");
        //System.out.println(thirdPartyResponse);
        
        // Mock
        webServer.enqueue(new MockResponse().setResponseCode(200)
            .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .setBody(thirdPartyResponse)
        );
        
        System.out.println(client.getDummyEntityApiResponse(webServer.getPort()));
        
        assertEquals(0, 0);
        
    }

}

项目结构

Pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.stackoverflow.q69665171</groupId>
    <artifactId>69665171</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>69665171</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>mockwebserver</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

上面的相同示例适用于 Jackson,但不是使用 ParameterizedTypeReference,而是使用 TypeReference 类,但为了优化,最好使用相同的 WebClient 执行反序列化。

【讨论】:

  • 我无法控制响应对象。例如'Inquiry' 对象不能用任何 Jackson 注释修改。第三方 API 响应对象是使用其 YAML 架构生成的。
  • 你能解释得更好吗?在示例中,您可以扩展到已定义泛型的任何规范。因此,如果 API 返回的实体不是 Inquery,您可以使用 ParameterizedTypeReference 在响应中指定它。
  • 现在,例如,如果要调用一个响应您不知道的不同实体的 EndPoint(这违反了 REST 规范),您可以做的是遵循示例的相同动态,但在解析之前您应该通过正则表达式的响应定义要反序列化的目标实体。
  • 但在正常情况下,您会提前知道 API 将响应的可能实体,因此通过上面的示例,您可以处理它以通用方式返回的不同类型的可能实体。
  • 在带有 JsonAlias 注释的 ThirdPartyAPIResponse 类中,您可以定义实体可以采用的所有不同名称,例如“查询”、“其他实体名称”。
【解决方案2】:

没有将泛型类型设置为“查询”的原因可能是什么 用于 Data.java 中的 @JsonProperty(DataNamingStrategy.DATA_FIELD)

原因是默认JsonProperty注解的属性名不能被DataNamingStrategy重命名。 Jackson 有this 功能,默认是禁用的。

ALLOW_EXPLICIT_PROPERTY_RENAMING 启用后将允许的功能 显式命名的属性(即,用注释的字段或方法 JsonProperty("explicitName")) 被重命名为 PropertyNamingStrategy(如果已配置)。功能被禁用 默认。

因为: 2.7

您需要做的就是启用此功能 -

ObjectMapper mapper = new ObjectMapper();
mapper.enable(MapperFeature.ALLOW_EXPLICIT_PROPERTY_RENAMING);

这里是相关的杰克逊笔记历史(引用自here

Prior versions allowed explicit property renaming by default
v2.4 - Jackson stopped allowing property renaming. (#428)
v2.7 - Introduced ALLOW_EXPLICIT_PROPERTY_RENAMING feature to allow / disallow Explicit Property renaming (#918)

   

【讨论】:

  • 像魅力一样工作!
猜你喜欢
  • 1970-01-01
  • 2014-05-19
  • 2017-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多