Reading JSON object with jackson - java

I would like to deserialize the following json object with jackson:
{
"_embedded": {
"endpoints": [
{
"name": "Tester",
"id": "48aba1b3-3585-4327-a20f-627a1749611b",
"componentId": "Darwin2",
"_links": {
"self": {
"href": "www.google.com"
},
"network": {
"href": "www.google.com"
},
"appWans": {
"href": "www.google.com"
},
"services": {
"href": "www.google.com"
},
"endpointGroups": {
"href": "www.google.com"
},
"geoRegion": {
"href": "www.google.com"
},
"dataCenter": {
"href": "www.google.com"
}
}
}
]
},
"_links": {
"first": {
"href": "www.google.com"
},
"last": {
"href": "www.google.com"
}
},
"page": {
"size": 2000,
"totalElements": 1,
"totalPages": 1,
"number": 1
}
}
My goal is to implement an Embedded object then within this object add another object called Endpoints. Ideally, I'd be able to access the id property off of the endpoints object. However, I keep getting deserialization errors. For the moment I am using this class:
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
#lombok.Value
public class Endpoints {
#JsonProperty("_embedded")
private Map<String, Object> embedded = new HashMap<>();
}
This at least affords me the opportunity to do the following:
Endpoints result = apiRequest.get();
if (result != null) System.out.println(result.getEmbedded().get("endpoints"));
Which prints out the array of endpoints, but I can't use this. I must implement a java object. Any help would be greatly appreciated with this issue.

So I have no idea what frameworks you use, or where this JSON data comes from, but you mentioned Spring HATEOS, so here is a solution using Jackson and Spring HATEOAS:
#Data
public class Endpoint extends RepresentationModel<Endpoint> {
private String name;
private String id; // Could be a UUID type instead??
private String componentId;
}
#Service
public class MyService {
#Autowired
private ObjectMapper objectMapper;
public void foo() {
String mysteryString = apiRequest.get();
PagedModel<Endpoint> endpointsPage = objectMapper.readValue(mysteryString, new TypeReference<PagedModel<Endpoint>>);
for (Endpoint e : endpointsPage) {
System.out.println(e.getName());
}
}
}
Spring HATEOAS docs and a guide. Also look at the Javadoc of the classes I've used.
If you want Endpoint to be a Lombok Value (i.e., all final), you need a constructor with appropriate ´#JsonCreator´ and ´#JsonProperty´ annotations, so Jackson knows how to build your Object (see 'guide' link).
And some more reading on Jackson.
Then again, if you are using Spring and you could just use Spring RestTemplate to fetch the Data from the remote API, you don't even need to manually use Jackson ObjectMapper:
PagedModel<Endpoint> endpointsPage =
restTemplate.exchange(apiUrl,
HttpMethod.GET,
null, // RequestEntity (body of request)
new ParameterizedTypeReference<PagedModel<Endpoint>>() {}).getBody();
The whole TypeReference and ParameterizedTypeReference business is only needed bc. we are dealing with generics.

Related

How to change collection format when using Swagger and Spring Fox?

I am using Spring Fox to generate the OpenApi 3.0 document for Swagger-ui.
With springfox-boot-starter:3.0.0
#GetMapping("/test")
public void test(TestParams testParams) {
}
public class TestParams {
List<String> list1;
}
When I get the json api-docs.I got this
{
// ...
"parameters": [
{
"name": "list1",
"in": "query",
"required": false,
"style": "pipeDelimited", << How to modify this to simple?
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
]
// ...
}
Now I need comma separator for this query field, not pipe separator.
See https://swagger.io/docs/specification/serialization/
So How to modify the style 'pipeDelimited' to 'simple'?
Thanks for help!

Configuring #RepositoryRestResource path with multi-segment for nested relationships

I've created 2 HATEOS #RepositoryRestResource for a Customer that has many Address :
class Customer {
...
Long id;
#OneToMany(
Set<Address> addresses;
...
}
class Address {
...
Long id;
...
}
#RepositoryRestResource(path = "customers")
public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long> {
}
#RepositoryRestResource(path = "addresses")
public interface AddressRepository extends PagingAndSortingRepository<Address, Long> {
}
I am able to get an Address entity with the following multi-segment path
Get Address 1 of Customer 1: GET /customers/1/adresses/1
BUT ... I'm not able to PATCH, PUT or DELETE an address using the same multi-segment path
Update Address 1 of Customer 1: PATCH, PUT or DELETE /customers/1/adresses/1
I always get a 404 - Not Found. It only works if I use the address endpoint
Update Address 1 : PATCH, PUT or DELETE /adresses/1
Is there a way to configure the #RepositoryRestResource so that I can update/delete an address (or any entities related to customer via the customer endpoint (/customers/{customerId}/addresses/{addressId}) ?
Regards
The code is available at https://github.com/fdlessard/spring-boot-repository-rest-resource
When you GET /customers/1/addresses/1, the response JSON should be
{
"country": "XYZ",
"_links": {
"self": {
"href": "http://localhost:8888/addresses/1" <-- here
},
"address": {
"href": "http://localhost:8888/addresses/1"
}
}
}
Use the link self, that is, http://localhost:8888/addresses/1 for update.
Actually, when you GET /customers/1/addresses, the array addresses in response JSON already gives you the true URI /addresses/1.
{
"_embedded": {
"addresses": [
{
"country": "XYZ",
"_links": {
"self": {
"href": "http://localhost:8888/addresses/1" <-- here
},
"address": {
"href": "http://localhost:8888/addresses/1"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8888/customers/1/addresses"
}
}
}
Remember, when play with HATEOAS, discover link and follow the href, rather than hardcode the URI. See https://docs.spring.io/spring-hateoas/docs/current/reference/html/#client.link-discoverer

Problems consuming json+hal _embedded resources with spring RestTemplate

I have a simple use case where I would like to consume a resource collection that is represented with json+hal.
I use the spring RestTemplate and have configuired it to use the Jackson2HalModule.
When I debug my code I find that the Response object does contain accurate metadata (e.g. number of pages and resources) and response headers but there is no content or links. I have looked at many articles and guides on the internet over the last day, and I feel that my custom rest template should be working for my use case based on my findings.
If anybody can shed any light on this I would be eternally grateful.
My code for my service is as follows:
#Service
public class EventServiceImpl extends BaseService implements EventService {
private static final String knownEntity = "59d786d642572853721728f6";
private static String SERVICE_URL = "http://EVENTS-SERVER";
private static String EVENTS_PATH = "/events";
#Autowired
#LoadBalanced
protected RestTemplate restTemplate;
#Override
public ResponseEntity<PagedResources<Event>> fetchEventsList() {
// acceptable media type
List<MediaType> acceptableMediaTypes = Arrays.asList(HAL_JSON);
// header
HttpHeaders headers = new HttpHeaders();
headers.setAccept(acceptableMediaTypes);
HttpEntity<String> entity = new HttpEntity<String>(null, headers);
ResponseEntity<PagedResources<Event>> response = getRestTemplateWithHalMessageConverter()
.exchange(SERVICE_URL + EVENTS_PATH, HttpMethod.GET, entity, new ParameterizedTypeReference<PagedResources<Event>>(){});
return response;
}
public RestTemplate getRestTemplateWithHalMessageConverter() {
List<HttpMessageConverter<?>> existingConverters = restTemplate.getMessageConverters();
List<HttpMessageConverter<?>> newConverters = new ArrayList<>();
newConverters.add(getHalMessageConverter());
newConverters.addAll(existingConverters);
restTemplate.setMessageConverters(newConverters);
return restTemplate;
}
private HttpMessageConverter getHalMessageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Jackson2HalModule());
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
halConverter.setObjectMapper(objectMapper);
return halConverter;
}
And my simple model is:
public class Event {
private String name;
private String location;
private int capacity;
public String getName() {
return name;
}
public String getLocation() {
return location;
}
public int getCapacity() {
return capacity;
}
}
For completeness, here is a samle of the shape of the hal+json I am attempting to consume:
{
"_embedded": {
"events": [
{
"name": null,
"location": null,
"capacity": 0,
"currentState": "CANCELLED",
"_links": {
"self": {
"href": "http://192.168.1.6:2221/events/59d786d642572853721728f6"
},
"event": {
"href": "http://192.168.1.6:2221/events/59d786d642572853721728f6"
},
"reinstate": {
"href": "http://192.168.1.6:2221/events/59d786d642572853721728f6/reinstate"
},
"reschedule": {
"href": "http://192.168.1.6:2221/events/59d786d642572853721728f6/reschedule"
}
}
},
{
"name": null,
"location": null,
"capacity": 0,
"currentState": "ADVERTISED",
"_links": {
"self": {
"href": "http://192.168.1.6:2221/events/59d7f14342572812ceca7fc6"
},
"event": {
"href": "http://192.168.1.6:2221/events/59d7f14342572812ceca7fc6"
},
"cancel": {
"href": "http://192.168.1.6:2221/events/59d7f14342572812ceca7fc6/cancel"
},
"reschedule": {
"href": "http://192.168.1.6:2221/events/59d7f14342572812ceca7fc6/reschedule"
}
}
},
{
"name": null,
"location": null,
"capacity": 0,
"currentState": "ADVERTISED",
"_links": {
"self": {
"href": "http://192.168.1.6:2221/events/59d7f14742572812ceca7fc7"
},
"event": {
"href": "http://192.168.1.6:2221/events/59d7f14742572812ceca7fc7"
},
"cancel": {
"href": "http://192.168.1.6:2221/events/59d7f14742572812ceca7fc7/cancel"
},
"reschedule": {
"href": "http://192.168.1.6:2221/events/59d7f14742572812ceca7fc7/reschedule"
}
}
},
{
"name": null,
"location": null,
"capacity": 0,
"currentState": "ADVERTISED",
"_links": {
"self": {
"href": "http://192.168.1.6:2221/events/59d7f14c42572812ceca7fc8"
},
"event": {
"href": "http://192.168.1.6:2221/events/59d7f14c42572812ceca7fc8"
},
"cancel": {
"href": "http://192.168.1.6:2221/events/59d7f14c42572812ceca7fc8/cancel"
},
"reschedule": {
"href": "http://192.168.1.6:2221/events/59d7f14c42572812ceca7fc8/reschedule"
}
}
}
]
},
"_links": {
"self": {
"href": "http://192.168.1.6:2221/events{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://192.168.1.6:2221/profile/events"
}
},
"page": {
"size": 20,
"totalElements": 4,
"totalPages": 1,
"number": 0
}
}
EDIT: I can consume an individual Event with no problems.
I had a similar problem and I ended up using org.springframework.hateoas.Resources
For my example below, this object is located in org.springframework.hateoas:spring-hateoas:jar:0.25.2.RELEASE
which is being pulled in from org.springframework.boot:spring-boot-starter-data-rest:jar:2.1.7.RELEASE, so there's a good chance its already in your classpath assuming you declare the spring-boot-starter-data-rest dependency.
Here's a simple example (using your info):
RestTemplate restTemplate = new RestTemplate();
Resources<Event> resourceEvents = restTemplate.getForObject("http://192.168.1.6:2221/events", Resources.class);
List<Event> events = new ArrayList<>(resourceEvents.getContent());
There are probably some gotchas the make this not so straight forward, but hopefully this provides a start to solving your problem.

Spring embedded reading json+hal from JSON content-type

"_embedded": {
"employees": [
{
"id": "1",
"name": "Some Employee",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
}
}
},
{
"id": "2",
"name": "Some Employee",
"_links": {
"self": {
"href": "http://localhost:8080/employees/2"
}
}
},
{
"id": "3",
"name": "Some Employee",
"_links": {
"self": {
"href": "http://localhost:8080/employees/3"
}
}
}
]
},
{
"_links": {
"self": {
"href": "http://localhost:8080/employees{?page,size,sort}"
},
"search": {
"href": "http://localhost:8080/employees/search"
}
},
"page": {
"size": 20,
"totalElements": 3,
"totalPages": 1,
"number": 0
}
}
I'm reading a third-party Rest service that returns json content with the above structure or similar (content-type is application/json).
Currently, I created a custom json converter to a generic object that has custom objects for links,pages and a generic type for the embedded object. So that, I read it in some sort of generic way. However, I would like to do it via hateoas objects. I tried several approaches but any of them are working.
Following others questions I tried many things like configuring the RestTamplate
#Bean
public OAuth2RestTemplate getOAuth2Rest() {
[...]
OAuth2RestTemplate template = new OAuth2RestTemplate(resourceDetails, clientContext);
List<HttpMessageConverter<?>> existingConverters = template.getMessageConverters();
List<HttpMessageConverter<?>> newConverters = new ArrayList<>();
newConverters.add(getHalMessageConverter());
newConverters.addAll(existingConverters);
template.setMessageConverters(newConverters);
return template;
}
private HttpMessageConverter getHalMessageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new Jackson2HalModule());
MappingJackson2HttpMessageConverter halConverter =
new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class);
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
halConverter.setObjectMapper(objectMapper);
return halConverter;
}
Then
ResponseEntity<Resources<MyDto>> entity = restTemplate.exchange(
myUri,
HttpMethod.GET, null,
new ParameterizedTypeReference<Resources<MyDto>>() {}, Collections.emptyMap());
There are similar questions like Consuming Spring Hateoas Pageable but they are consuming json+hal content. I think here is my problem because the deserializer uses the json converter and not the hal converter.
I tried
#EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
Didn't work and interferes with my REST configuration.
I tried this blog

Spring REST response is different in a custom controller

I have several controllers that are automatically creating REST endpoints.
#RepositoryRestResource(collectionResourceRel = "books", path = "books")
public interface BooksRepository extends CrudRepository<Books, Integer> {
public Page<Books> findTopByNameOrderByFilenameDesc(String name);
}
When I visit: http://localhost:8080/Books
I get back:
{
"_embedded": {
"Books": [{
"id": ,
"filename": "Test123",
"name": "test123",
"_links": {
"self": {
"href": "http://localhost:8080/books/123"
},
"Books": {
"href": "http://localhost:8080/books/123"
}
}
}]
},
"_links": {
"self": {
"href": "http://localhost:8080/books"
},
"profile": {
"href": "http://localhost:8080/profile/books"
},
"search": {
"href": "http://localhost:8080/books/search"
},
"page": {
"size": 20,
"totalElements": 81,
"totalPages": 5,
"number": 0
}
}
}
When I create my own controller:
#Controller
#RequestMapping(value = "/CustomBooks")
public class CustomBooksController {
#Autowired
public CustomBookService customBookService;
#RequestMapping("/search")
#ResponseBody
public Page<Book> search(#RequestParam(value = "q", required = false) String query,
#PageableDefault(page = 0, size = 20) Pageable pageable) {
return customBookService.findAll();
}
}
I'll get a response back that looks nothing like the automatically generated controller response:
{
"content": [{
"filename": "Test123",
"name" : "test123"
}],
"totalPages": 5,
"totalElements": 81,
"size": 20,
"number": 0,
}
What do I need to do to make my response look like the automatically generated response? I want to keep it consistent, so I don't have to rewrite code for a different response. Should I be doing it a different way?
Edit: Found this: Enable HAL serialization in Spring Boot for custom controller method
But I don't understand what I need to change in my REST Controller to enable: PersistentEntityResourceAssembler. I've searched on Google for PersistentEntityResourceAssembler, but it keeps leading me back to similar pages without much of an example (or the example doesn't seem to work for me).
As #chrylis suggested you should replace your #Controller annotation with #RepositoryRestController for spring-data-rest to invoke it's ResourceProcessors for customizing the given resource.
For you resource to follow the HATEOAS specification (like your spring-data-rest BooksRepository) your method declaration return type should be like HttpEntity<PagedResources<Resource<Books>>>
For converting your Page object to PagedResources:
You need to autowire this object.
#Autowired
private PagedResourcesAssembler<Books> bookAssembler;
Your return statement should be like
return new ResponseEntity<>(bookAssembler.toResource(customBookService.findAll()), HttpStatus.OK);
These changes should help you to get a org.springframework.hateoas.Resources compliant response containing the "_embedded" and "_links" attribute.

Categories