Retrofit Jackson converter doesn't recognize nested objects - java

I am using RetrofitJackson2SpiceService to make requests in a service. Jackson is used to parse JSON responses from the API.
But I have one problem.
My User model has following declaration
#JsonIgnoreProperties(ignoreUnknown=true)
public class User {
#JsonProperty("id")
public int id;
#JsonProperty("name")
public String name;
#JsonProperty("surname")
public String surname;
#JsonProperty("email")
public String email;
#JsonProperty("phone")
public String phone;
#JsonProperty("BirthDate")
public String birthDate;
#JsonProperty("token_model")
public Token token;
}
As you may noticed this class has Token as a member
#JsonIgnoreProperties(ignoreUnknown=true)
public class Token {
#JsonProperty("token")
public String token;
#JsonProperty("expiration_time")
public int expirationTime;
#JsonProperty("scope")
public int scope;
}
Server response looks like this
{"id":"36","email":"something#yo.com","name":"Say","surname":"hello","login":"superuser","phone":"4534333","token_model":{"token":"a220249b55eb700c27de780d040dea28","expiration_time":"1444673209","scope":"0"}}
Token is not being parsed, it is always null.
I have tried to convert string manually
String json = "{\"id\":\"36\",\"email\":\"something#yo.com\",\"name\":\"Say\",\"surname\":\"hello\",\"login\":\"superuser\",\"phone\":\"4534333\",\"token_model\":{\"token\":\"a220249b55eb700c27de780d040dea28\",\"expiration_time\":\"1444673209\",\"scope\":\"0\"}}";
ObjectMapper mapper = new ObjectMapper();
User user = null;
try {
user = mapper.readValue(json, User.class);
} catch (IOException e) {
e.printStackTrace();
}
And it works ! Token is parsed correctly without any problems.
Here I have used readValue method accepting String as first parameter, but in Converter
JavaType javaType = objectMapper.getTypeFactory().constructType(type);
return objectMapper.readValue(body.in(), javaType);
Stream version of method is used.
I have tried to return Response instead of User object in the following way
public void onRequestSuccess(Response response) {
super.onRequestSuccess(response);
ObjectMapper objectMapper = new ObjectMapper();
User user = null;
try {
user = objectMapper.readValue(response.getBody().in(), User.class);
} catch (IOException e) {
e.printStackTrace();
}
}
And it works great, like it should, token is parsed correctly.
I have no idea what can cause such problem, I have tried a lot of different combinations of annotations(custom deserializers, unwrap....), custom converters but still the same.
I would be grateful for any help.
Thanks.

I have found the problem exploring source code of Retrofit
The problem is that even if my service is inherited from RetrofitJackson2SpiceService it doesn't use JacksonConverter by default.
GsonConverter used instead.
mRestAdapterBuilder = new RestAdapter.Builder()
.setEndpoint(getServerUrl())
.setConverter(createConverter()) //this line
.setRequestInterceptor(new AuthRequestInterceptor(context))
.setClient(new OkClient(mHttpClient))
.setLogLevel(RestAdapter.LogLevel.FULL)
.setLog(new AndroidLog("RETROFIT"));
Adding converter explicitly while building rest adapter solved the problem.

Related

Downcasting a CompletableFuture's object type - Java/Spring Boot

I have the following classes:
public class AccountDetail {
private String accountNumber;
private Date effectiveDate;
private String status;
// a bunch of other properties
}
public class AccountDetailWithAlerts extends AccountDetail {
private LowMediumAlerts alerts;
}
public class AccountsAndAlerts {
private List<AccountDetailWithAlerts> accounts;
private HighCustomerAccountAlerts accountAlerts;
// getters and setters
}
public class CustomerAndAccountAlerts {
private List<AlertMessage> customerAlerts;
private List<AccountAlertMessages> accountAlerts;
}
public Class CompanyResponse<T> {
#JsonInclude(JsonInclude.Include.NON_NULL)
private T response;
// other things that aren't relevant
}
I have a controller, AccountsController, that does a #GetMapping and has a ResponseEntity method:
public ResponseEntity<CompanyResponse<AccountsAndAlerts> getAccountDetails {
#RequestParam MultiValueMap<String, String> queryParms,
// some #ApiParams for client-header, end-user-id & accountNumber
String accountId = queryParms.getFirst("accountId");
// setting RestHeaders, contentType
CompanyResponse<AccountsAndAlerts> response = accountDetailService.getAccountsWithAlerts(restHeaders, accountNumber, queryParms, accountId);
return new ResponseEntity<CompanyResponse<AccountsAndAlerts>>(response, headers, HttpStatus.valueOf(response.getStatus()));
}
Here is the method in accountDetailService:
public CompanyResponse<AccountsAndAlerts> getAccountsWithAlerts(RestHeaders restHeaders, String accountNumber, MultiValueMap<String, String> queryParms, String accountId) throws... {
CompanyResponse<AccountsAndAlerts> newResponse = new CompanyResponse<AccountsAndAlerts>();
try {
CompletableFuture<List<AccountDetailWithAlerts>> accountsFuture = accountDetails.getAccounts(newResponse, restHeaders, accountNumber, queryParms);
CompletableFuture<CustomerAndAccountAlerts> alertsFuture = accountDetails.getAlerts(newResponse, restHeaders, accountId);
accountsFuture.thenAcceptBoth(alertsFuture, (s1, s2) -> newResponse.setResponse(getResponse(s1, s2))).get();
} catch {
// catch code
}
return newResponse;
}
Finally, the getAccounts method in AccountDetails:
public CompletableFuture<List<AccountDetailWithAlerts>> getAccounts(CompanyResponse<AccountsAndAlerts> newResponse, RestHeaders restHeaders, String accountNumber, MultiValueMap<String, String> queryParms) throws ... {
// this has the restTemplate and the .supplyAsync()
}
What I need to do is create a new ResponseEntityMethod in the Controller:
public ResponseEntity<CompanyResponse<AccountDetail> getCertainAccountDetails
I have put in a return of that type, and I am attempting to create a new method in the accountDetailService, getCertainAccounts().
The problem is trying to set this all up without creating a whole other CompletableFuture method with an invoke and supplyAsync() and restTemplate and such.
It appears that I still need to call getAccounts(), but then I have to somewhere along this line downcast the AccountDetailWithMessages to AccountDetail. I don't know if I can somehow downcast CompletableFuture<List<AccountDetailWithAlerts>> to CompletableFuture<List<AccountDetail>> or how to do it, or if I really need to downcast CompanyResponse<AccountsAndAlerts> or how to do that.
Can anyone help?
PS. I changed the names of everything to protect my Company's code. If you see errors in methods or names or anything, please be assured that is not an issue and is just the result of my typing things out instead of copying and pasting. The only issue is how to do the downcasting.
Thanks!
PPS. In case it wasn't clear, with my new method and code I do not want to get the alerts. I am trying to get account details only without alerts.

How can I set an optional RequestBody field without it being deleted when I make the call?

I have a small program in spring-boot which through a get call having a #RequestBody returns me a message with all the specifications (in my case of cars)
public class CarsRequest implements Serializable {
private String name;
private String plate ;
private String price;
}
I would like to be able to make sure that if a field is set to null, it can still find the relative message with the other fields having a value, in my case, I wanted to put that the "name" field is optional in the RequestBody, is it possible to do this? I tried setting
public CarsResponse getCars(#RequestBody (required = false) CarsRequest request) throws IOException {
//some code
}
but then when I go to do the get it completely deletes the null field at the time of the get and therefore fails to do it
Just remove the #RequestBody annotation from the function and keep it as it is
public CarsResponse getCars(CarsRequest request) throws IOException {
//some code
}
Now all fields will be converted into query params and all will be optional, because query param by convention are optional
public class CarsRequest implements Serializable {
private String name;
private String plate ;
private String price;
}
And call like this
GET /someEndpoint?name=<value>&plate=null
But still if you want to make some params mandatory, then use javax.annotations or apply validation yourself.
EDIT: As asked in comment, if you are accepting JSON as parameter body then you can do one thing, you can accept it as String and then convert json to object inside function body
public CarsResponse getCars(#RequestParam(required = false) String request) throws IOException {
ObjectMapper mapper = new ObjectMapper();
CarRequest request = mapper.readValue(request,CarRequest.class);
// other code
}
and call it something like this
GET /someEndpoint?request="{ \"name\" : null, \"plate\": \"someValue\" }"
EDIT 2:
You can do one more thing if you want to keep sending json and have it transformed into object, you can declare a binder something like this
// Some controller class
class SomeController {
#Autowired
ObjectMapper mapper;
// Ommited methods here
#GetMapping("/carRequest")
public ResponseEntity<String> testBinder(#RequestParam CarRequest request) {
return ResponseEntity.ok("{\"success\": \"" + request.name+ "\"}");
}
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(CarRequest.class, new CarRequestEditor(mapper));
}
static class CarRequestEditor extends PropertyEditorSupport {
private ObjectMapper objectMapper;
public CarRequestEditor(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
#Override
public void setAsText(String text) throws IllegalArgumentException
{
if (StringUtils.isEmpty(text)) {
setValue(new CarRequest());
} else {
try {
setValue(objectMapper.readValue(text, CarRequest.class));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
}
}
}
Please note that the client need to send the json URL encoded like this
http://localhost:8180/carRequest?request=%7B%22name%22%3"test"%7D
Hi you are using #RequestBody (required = false) CarsRequest
that means your CarsRequest object itself is optional
rather than you can use
#NotEmpty
private String plate ;
#NotEmpty
private String price;
You can make a single field optional by making it an Optional, in your case Optional<String>. If the field does not appear in the request body, then the Optional will be empty.
public class CarsRequest implements Serializable {
private String name;
private String plate;
private Optional<String> price;
}

Object rendering to Json - Spring Boot

I am currently working on an SDK for integrating with an API (as Client). My problem is (I am not even sure this is a problem), but I wanted my request to only contain the parameters that are initialized. Example of how they are being generated now:
{
"scenarioKey":"",
"bulkId":"",
"destinations":[
{
"messageId":"xxxxx",
"to":{
"phoneNumber":""
}
}
],
"sms":null
}
The SMS parameter was never initiated hence I wanted it not to be included in the request body, is that a way I can have a request without this parameter "sms"?
By the way I am using HttpEntity:
HttpEntity<Object> entity = new HttpEntity<>(bodyObject, headers);
Spring boot allows simple configuration of the Jackson ObjectMapper it uses in the application.properties file.
The supported properties are described in the documentation. (https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-customize-the-jackson-objectmapper)
Specifically spring.jackson.default-property-inclusion=non_null in the application.properties should do the trick.
If you want to do this for specific classes or attributes Jackson has the annotation #JsonInclude(Include.NON_NULL).
If you are using jackson to serialize your JSON, you should take a look at the setSerializationInclusion() method on ObjectMapper https://fasterxml.github.io/jackson-databind/javadoc/2.7/com/fasterxml/jackson/databind/ObjectMapper.html#setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include)
Here is a simple testcase that prevents the sms field from being included in the JSON output:
#Test
public void testJson() throws Exception {
Addr addr = new Addr();
addr.phoneNumber = "";
Destination destination = new Destination();
destination.messageId = "";
destination.to = addr;
Scenario scenario = new Scenario();
scenario.scenarioKey = "";
scenario.bulkId = "";
scenario.destinations = Arrays.asList(destination);
ObjectMapper mapper = new ObjectMapper()
.enable(SerializationConfig.Feature.INDENT_OUTPUT)
.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
System.out.println(mapper.writeValueAsString(scenario));
}
public static class Scenario {
public String scenarioKey;
public String bulkId;
public List<Destination> destinations;
public String sms;
}
public static class Destination {
public String messageId;
public Addr to;
}
public static class Addr {
public String phoneNumber;
}

Parse Json with com.fasterxml.jackson instead of org.json

I was wondering if it is possible to do this exact operation but with the jackson library.
String repo = response.toString();
JSONObject json = new JSONObject (repo);
String nameOfUser = json.getJSONObject(facebookID).getString("name");
Thank you,
Yes. Something like:
ObjectMapper mapper = new ObjectMapper(); // reuse, usually static final
JsonNode ob = mapper.readTree(response.toString()); // or from File, URL, InputStream, Reader
String nameOfUser = ob.path(facebookID).path("name").asText();
// note: '.get()' also works, but returns nulls, 'path()' safer
although even more convenient access is often done using JSON Pointer expressions, like:
String name = ob.at("/person/id").asText();
but I assume facebookID is an id from some other source.
UPDATE: as per comment below, structure you want may actually be POJO like:
public class Response {
public User facebookID;
}
public class User {
public String id;
public String email;
public String first_name;
// ... and so forth: fields and/or getter+setter
}
and then you can bind directly into class like so:
Response resp = mapper.readValue(response.toString(), Response.class);
String name = resp.facebookID.name;
So there's more than one way to do it with Jackson.

How to return a partial JSON response using Java?

I'm building a RESTful API and want to provide developers with the option to choose which fields to return in the JSON response. This blog post shows examples of how several API's (Google, Facebook, LinkedIn) allow developers to customize the response. This is referred to as partial response.
An example might look like this:
/users/123?fields=userId,fullname,title
In the example above the API should return the userId, fullName and title fields for User "123".
I'm looking for ideas of how to implement this in my RESTful web service. I'm currently using CXF (edit: and Jackson) but willing to try another JAX-RS implementation.
Here's what I currently have. It returns a full User object. How can I return only the fields the API caller wants at runtime based on the "fields" paramaeter? I don't want to make the other fields Null. I simply don't want to return them.
#GET
#Path("/{userId}")
#Produces("application/json")
public User getUser(#PathParam("userId") Long userId,
#DefaultValue("userId,fullname,title") #QueryParam("fields") String fields) {
User user = userService.findOne(userId);
StringTokenizer st = new StringTokenizer(fields, ",");
while (st.hasMoreTokens()) {
// here's where i would like to select only the fields i want to return
}
return user;
}
UPDATE:
I followed unludo's link which then linked to this: http://wiki.fasterxml.com/JacksonFeatureJsonFilter
With that info I added #JsonFilter("myFilter") to my domain class. Then I modified my RESTful service method to return String instead of User as follows:
#GET
#Path("/{userId}")
#Produces("application/json")
public String getUser(#PathParam("userId") Long userId,
#DefaultValue("userId,fullname,title") #QueryParam("fields") String fields) {
User user = userService.findOne(userId);
StringTokenizer st = new StringTokenizer(fields, ",");
Set<String> filterProperties = new HashSet<String>();
while (st.hasMoreTokens()) {
filterProperties.add(st.nextToken());
}
ObjectMapper mapper = new ObjectMapper();
FilterProvider filters = new SimpleFilterProvider().addFilter("myFilter",
SimpleBeanPropertyFilter.filterOutAllExcept(filterProperties));
try {
String json = mapper.filteredWriter(filters).writeValueAsString(user);
return json;
} catch (IOException e) {
e.printStackTrace();
return e.getMessage();
}
}
I need to do more testing but so far so good.
If you use Jackson (a great JSON lib - kind of the standard for Java I believe), you may use the #View annotation to filter what you want in the resulting object.
I understand that you want something dynamic so it's a bit more complicated. You will find what you are looking for here: http://www.cowtowncoder.com/blog/archives/2011/02/entry_443.html (look at 6. Fully dynamic filtering: #JsonFilter).
I would be interested in the solution you will find.
Creating an ObjectMapper instance inside the resource method for every request can have significant performance overhead. According to the Jackson performance best practices object mappers are expensive to create.
Instead you can customize the JAX-RS provider's Jackson object writer inside the resource method using the Jackson 2.3 ObjectWriterModifier/ObjectReaderModifier feature.
Here is an example shows how to register an ObjectWriterModifier thread local object that changes the set of the filters applied for the JAX-RS Jackson provider being used inside a resource method. Note that I have not tested the code against an JAX-RS implementation.
public class JacksonObjectWriterModifier2 {
private static class FilterModifier extends ObjectWriterModifier {
private final FilterProvider provider;
private FilterModifier(FilterProvider provider) {
this.provider = provider;
}
#Override
public ObjectWriter modify(EndpointConfigBase<?> endpoint, MultivaluedMap<String, Object> responseHeaders,
Object valueToWrite, ObjectWriter w, JsonGenerator g) throws IOException {
return w.with(provider);
}
}
#JsonFilter("filter1")
public static class Bean {
public final String field1;
public final String field2;
public Bean(String field1, String field2) {
this.field1 = field1;
this.field2 = field2;
}
}
public static void main(String[] args) throws IOException {
Bean b = new Bean("a", "b");
JacksonJsonProvider provider = new JacksonJsonProvider();
ObjectWriterInjector.set(new FilterModifier(new SimpleFilterProvider().addFilter("filter1",
SimpleBeanPropertyFilter.filterOutAllExcept("field1"))));
provider.writeTo(b, Bean.class, null, null, MediaType.APPLICATION_JSON_TYPE, null, System.out);
}
}
Output:
{"field1":"a"}
The Library jersey-entity-filtering Can do that :
https://github.com/jersey/jersey/tree/2.22.2/examples/entity-filtering-selectable
https://jersey.java.net/documentation/latest/entity-filtering.html
Exemple :
My Object
public class Address {
private String streetAddress;
private String region;
private PhoneNumber phoneNumber;
}
URL
people/1234?select=streetAddress,region
RETURN
{
"streetAddress": "2 square Tyson",
"region": "Texas"
}
Add to Maven
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-entity-filtering</artifactId>
<version>2.22.2</version>
</dependency>

Categories

Resources