具有变量 Json 响应对象和 Json 属性 的 Jackson 泛型

Jackson Generics with variable Json response object and Json property

根据 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" 
        } 
    }
}

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

Inquiry.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
}

Meta.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);
    }
}

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

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

以下代码的响应(查询和 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.Whosebug.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.Whosebug.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;
    
}

税收频率

package com.Whosebug.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.Whosebug.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 实现)

package com.Whosebug.q69665171.third_party;

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

import com.Whosebug.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();
    }

}

ThirdPartyAPIResponse

package com.Whosebug.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.Whosebug.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.Whosebug.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.Whosebug.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,但使用的是 TypeReference class 而不是 ParameterizedTypeReference,但为了优化,最好使用相同的 WebClient 执行反序列化。

What could be the reason for not setting the generic type to "Inquiry" for @JsonProperty(DataNamingStrategy.DATA_FIELD) in Data.java

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

ALLOW_EXPLICIT_PROPERTY_RENAMING Feature that when enabled will allow explicitly named properties (i.e., fields or methods annotated with JsonProperty("explicitName")) to be re-named by a PropertyNamingStrategy, if one is configured. Feature is disabled by default.

Since: 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)