Reputation: 5201
Based on the answer 'Jackson Generics with variable JsonProperty' by VizGhar, I am trying to use WebClient to consume an API response which contains a generic object in json response:
{
"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"
}
}
}
Where type of "Inquiry" would be generic, i.e. "data" is a wrapper around a generic response object which is 'Inquiry' in this case, but it may change.
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
}
My Response.java looks like this:
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
}
My Controller:
@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);
}
}
This is not working for me. What could be the reason for not setting the generic type to "Inquiry" for @JsonProperty(DataNamingStrategy.DATA_FIELD) in Data.java
Upvotes: 1
Views: 1441
Reputation: 2030
What could be the reason for not setting the generic type to "Inquiry" for @JsonProperty(DataNamingStrategy.DATA_FIELD) in Data.java
The reason is that by default JsonProperty
annotated property name cannot be renamed by DataNamingStrategy
. Jackson has this feature which is disabled by default.
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
All you need to do is to enable this feature -
ObjectMapper mapper = new ObjectMapper();
mapper.enable(MapperFeature.ALLOW_EXPLICIT_PROPERTY_RENAMING);
Here is the related Jackson notes history (referred from 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)
Upvotes: 4
Reputation: 407
You can do the deserialization directly with the WebClient and with the ParameterizedTypeReference class. Below is your working example:
Response from the code below (Inquiry and 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)))
Inquiry
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;
}
Dummy Entity
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();
}
}
ThirdPartyAPIResponse
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 Dummy Entity
{
"meta":{
"RID":"abc9f-defgh-hj78k-lkm9n",
"QID":"abc9f-defgh-hj78k-lkm9n"
},
"data":{
"DummyEntity":{
"code": 200,
"message": "Hello World"
}
}
}
Test
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);
}
}
Project Structure
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>
The same example above works with Jackson but instead of ParameterizedTypeReference, the TypeReference class is used but for optimization, it's better to perform deserialization with the same WebClient.
Upvotes: 0