I have two apis which is return following json responses.
I created a class called 'Card' and how should I achieve 'expiry' field which have different type for particular request.
Thanks in advance.
CodePudding user response:
Can you please share how the expiry field is prepared? like
"expiery":"0139" = month year. if the field is prepared like this then you can keep variable type either String or list, you have needed a minor modification. You will give an idea according to the given example.
public class App {
private static final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(10))
.build();
public static void main(String[] args) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://jsonmock.abc.com/api/articles?page=3"))
.setHeader("User-Agent", "Java 11 HttpClient Bot") // add request header
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// print response headers
HttpHeaders headers = response.headers();
headers.map().forEach((k, v) -> System.out.println(k ":" v));
String responseBody = response.body();
ObjectMapper mapper = new ObjectMapper();
Object newJsonNode = mapper.readValue(responseBody, Object.class);
Map<String, Object> objectMap = (Map) newJsonNode;
if (objectMap.get("expiry") instanceof List) {
//if I assume that expiry string created by appending month and year
List<Map<String, Object>> list = (List) objectMap.get("expiry");
Map<String, Object> map = list.get(0);
String exp = (String) map.get("month") (String) map.get("year");
objectMap.put("expiry", exp);
//then parse to direct object
} else if (objectMap.get("expiry") instanceof String) {
//then cast to dto
//you can apply reverse logic here, i.e string to map
}
}
}
CodePudding user response:
For simplicity i'll work only with card object from example, i won't be wrapping it another object. To avoid duplication in examples, i'll use this base class to hold common fields:
public abstract class BaseCard {
private String brand;
private String fundingMethod;
private String scheme;
//setters and getters
}
You have few options.
Option 1:
If you know which api you are getting response from, you can have two classes, each holding expiry in format specific for the api. When getting data from api 1 use:
public class CardApi1 extends BaseCard {
private String expiry;
//setters and getters
}
And for api 2:
public class CardApi2 extends BaseCard {
private Expiry expiry;
//setters and getters
}
The expiry object looks like this:
public class Expiry {
private String month;
private String year;
//setters and getters
}
If you are calling api 1, deserialize to CardApi1, else to CardApi2.
Option 2: Make the expiry field Object, this way anything can be deserialized into it.
public class CardApiMixed extends BaseCard {
private Object expiry;
//setters and getters
public String getExpiryAsString() {
return (String) this.expiry;
}
public Map<String, Object> getExpiryAsMap() {
@SuppressWarnings("unchecked")
Map<String, Object> expiry = (Map<String, Object>) this.expiry;
return expiry;
}
}
This way no matter which api you are calling, you deserialize into one class. The downside is, when retrieving expiry, you still have to know which api the data comes from, so you use correct merhod.
Option 3: Write custom deserializer which will resolve the field correctly, no matter which api is used. Personally i would go for this option. The deserializer is this:
public class ExpiryResolvingCardDeserializer extends StdDeserializer<CardApiResolved> {
public ExpiryResolvingCardDeserializer() {
super(CardApiResolved.class);
}
@Override
public CardApiResolved deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
CardApiResolved card = new CardApiResolved();
card.setBrand(node.get("brand").asText());
card.setFundingMethod(node.get("fundingMethod").asText());
card.setScheme(node.get("scheme").asText());
JsonNode expiryNode = node.get("expiry");
Expiry expiry = new Expiry();
if (expiryNode.isObject()) {
//that's for api 2
expiry.setMonth(expiryNode.get("month").asText());
expiry.setYear(expiryNode.get("year").asText());
} else {
//for api 1
String text = expiryNode.asText();
//assuming format is always mmYY, you can handle it in differently if there are other options
int splitIndex = 2;
expiry.setMonth(text.substring(0, splitIndex));
expiry.setYear(text.substring(splitIndex));
}
card.setExpiry(expiry);
return card;
}
}
To summarise it, when expiry node is object, handle it like an object to get month and year data from it, if it is a string, split it to extract month and year. Like this no matter the format, expiry is always resolved to Expiry class. The card class will look like this:
@JsonDeserialize(using = ExpiryResolvingCardDeserializer.class)
public class CardApiResolved extends BaseCard {
private Expiry expiry;
//setters and getters
}
Note the JsonDeserialize annotation specifying the deserializer to use for this type. And lastly, some unit tests to play with and check results. The api responses are files in the test resources.
public class CardApiTests {
private final ObjectMapper mapper = new ObjectMapper();
@Test
public void testCardApi1() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-1.json");
//map to CardApi1, when calling api 1
CardApi1 result = this.mapper.readValue(inputStream, CardApi1.class);
assertEquals("0139", result.getExpiry());
}
@Test
public void testCardApi2() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-2.json");
//map to CardApi2, when calling api 2
CardApi2 result = this.mapper.readValue(inputStream, CardApi2.class);
assertEquals("1", result.getExpiry().getMonth());
assertEquals("39", result.getExpiry().getYear());
}
@Test
public void testCardApiMixed_Api1() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-1.json");
CardApiMixed result = this.mapper.readValue(inputStream, CardApiMixed.class);
assertEquals("0139", result.getExpiryAsString());
}
@Test
public void testCardApiMixed_Api2() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-2.json");
CardApiMixed result = this.mapper.readValue(inputStream, CardApiMixed.class);
assertEquals("1", result.getExpiryAsMap().get("month"));
assertEquals("39", result.getExpiryAsMap().get("year"));
}
@Test
public void testCardApiResolved_Api1() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-1.json");
CardApiResolved result = this.mapper.readValue(inputStream, CardApiResolved.class);
assertEquals("01", result.getExpiry().getMonth());
assertEquals("39", result.getExpiry().getYear());
}
@Test
public void testCardApiResolved_Api2() throws Exception {
InputStream inputStream = ClassLoader.getSystemResourceAsStream("card-api-2.json");
CardApiResolved result = this.mapper.readValue(inputStream, CardApiResolved.class);
assertEquals("1", result.getExpiry().getMonth());
assertEquals("39", result.getExpiry().getYear());
}
}

