spring-projects / spring-hateoas

Spring HATEOAS - Library to support implementing representations for hyper-text driven REST web services.
https://spring.io/projects/spring-hateoas
Apache License 2.0
1.05k stars 477 forks source link

Spring HATEOAS does not respect default inclusion property for resource link #766

Closed anadimisra closed 5 years ago

anadimisra commented 5 years ago

I have the problem similar to one asked in this question however, applying the suggested solution

spring.jackson.default-property-inclusion=NON_NULL does not stop HATEOAS from rendering links with null properties. Here's my controller declaration

@RestController
@ExposesResourceFor(Customer.class)
public class CustomerController {
  // controller methods here
}

and the web config class

@Configuration
@EnableSpringDataWebSupport
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class DataApiWebConfiguration extends WebMvcConfigurationSupport {
  // config here
}

In the Controller get method that returns a resource I declare mapping as follows

@GetMapping(value = "/customers/{id}", produces = MediaTypes.HAL_JSON_VALUE)

and then I return a Resource

Optional<Customer> customer = customerRepository.findById(id);
return customer.map(customerResourceAssembler::toResource).map(ResponseEntity::ok)
                            .orElse(ResponseEntity.notFound().build());

The CustomerResourceAssembler extends SimpleIdentifiableResourceAssembler as demonstrated in the spring-hateaos example.

But in the response body I still see links rendered with null properties

"links": [
            {
                "rel": "self",
                "href": "http://localhost:8080/customers/11",
                "hreflang": null,
                "media": null,
                "title": null,
                "type": null,
                "deprecation": null
            }
]

this doesn't look like how a HATEOAS response should be, like in examples I see links not _links in the JSON

gregturn commented 5 years ago

The fact that your JSON is showing links and not _links reveals that your configuration is not properly setup.

For starters, if you are using Spring Boot, then there is no need to use @EnableHypermediaSupport. In fact, by using the annotation directly, it instructs Spring Boot to back off and NOT activate its other hypermedia-supporting configurations. Instead, you have to do it all yourself.

If you remove that annotation from your configuration, what hypermedia do you then see?

anadimisra commented 5 years ago

Hi! @gregturn

Removing @EnableHypermediaSupport doesn't result in the removal of null fields from the rendered HAL JSON. Here's the full response for GET /customers call

{
    "links": [
        {
            "rel": "self",
            "href": "http://localhost:8080/data/api/customers",
            "hreflang": null,
            "media": null,
            "title": null,
            "type": null,
            "deprecation": null
        }
    ],
    "content": [
        {
            "name": "Minty And Sons Pvt. Ltd.",
            "pan": "5GB7W15M0T",
            "currecny": "INR",
            "tds": 0.1,
            "invoicePrefix": "INV",
            "links": [
                {
                    "rel": "self",
                    "href": "/customers/1",
                    "hreflang": null,
                    "media": null,
                    "title": null,
                    "type": null,
                    "deprecation": null
                },
                {
                    "rel": "customers",
                    "href": "/customers",
                    "hreflang": null,
                    "media": null,
                    "title": null,
                    "type": null,
                    "deprecation": null
                },
                {
                    "rel": "contact",
                    "href": "/customers/1/contact",
                    "hreflang": null,
                    "media": null,
                    "title": null,
                    "type": null,
                    "deprecation": null
                },
                {
                    "rel": "branches",
                    "href": "/customers/1/branches",
                    "hreflang": null,
                    "media": null,
                    "title": null,
                    "type": null,
                    "deprecation": null
                },
                {
                    "rel": "invoices",
                    "href": "/customers/1/invoices",
                    "hreflang": null,
                    "media": null,
                    "title": null,
                    "type": null,
                    "deprecation": null
                },
                {
                    "rel": "paid-invoices",
                    "href": "/customers/1/invoices/paid",
                    "hreflang": null,
                    "media": null,
                    "title": null,
                    "type": null,
                    "deprecation": null
                },
                {
                    "rel": "pending-invoices",
                    "href": "/customers/1/invoices/pending",
                    "hreflang": null,
                    "media": null,
                    "title": null,
                    "type": null,
                    "deprecation": null
                },
                {
                    "rel": "overdue-invoices",
                    "href": "/customers/1/invoices/overdue",
                    "hreflang": null,
                    "media": null,
                    "title": null,
                    "type": null,
                    "deprecation": null
                }
            ]
        }
    ],
    "page": {
        "size": 20,
        "totalElements": 1,
        "totalPages": 1,
        "number": 0
    }
}
gregturn commented 5 years ago

Again not producing HAL.

Which also happens if your controller doesn’t return a ResourceSupport type.

Can you show your controller?

anadimisra commented 5 years ago

I am returning Resource wrapped in ResponseEntity

@GetMapping(value = "/customers/{id}", produces = MediaTypes.HAL_JSON_VALUE)
public DeferredResult<ResponseEntity<Resource<Customer>>> getCustomer(@PathVariable Long id,
            HttpServletRequest request) {

    DeferredResult<ResponseEntity<Resource<Customer>>> response = new DeferredResult<>();
    response.onTimeout(() -> response
                .setErrorResult(ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body("Request timed out.")));
    response.onError((Throwable t) -> {
        response.setErrorResult(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occured."));
        });
    ListenableFuture<Optional<Customer>> future = customerService.findById(id);

    future.addCallback(new ListenableFutureCallback<Optional<Customer>>() {

        @Override
        public void onSuccess(Optional<Customer> customer) {
                response.setResult(customer.map(customerResourceAssembler::toResource).map(ResponseEntity::ok)
                        .orElse(ResponseEntity.notFound().build()));

        }

        @Override
        public void onFailure(Throwable ex) {
            LOGGER.error("Cannot get customer details for id {} due to error: {}", id, ex.getMessage(), ex);
                response.setErrorResult(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                        .body("Cannot get customer details due to server error."));
            }

    });
    return response;
}
anadimisra commented 5 years ago

The problem was in my configuration class, I was registering the Hibernate5Module in a wrong way, removed these lines

@Bean
    public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new HibernateAwareObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        jsonConverter.setObjectMapper(objectMapper);
        return jsonConverter;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(customJackson2HttpMessageConverter());
        super.addDefaultHttpMessageConverters(converters);
    }

and simply added a bean

    @Bean
    public Module hibernate5Module() {
        return new Hibernate5Module();
    }

that fixed the output

{
    "_embedded": {
        "customers": [
            {
                "name": "Minty And Sons Pvt. Ltd.",
                "pan": "5GB7W15M0T",
                "currecny": "INR",
                "tds": 0.1,
                "invoice_prefix": "INV",
                "_links": {
                    "self": {
                        "href": "/customers/1"
                    },
                    "customers": {
                        "href": "/customers"
                    },
                    "contact": {
                        "href": "/customers/1/contact"
                    },
                    "branches": {
                        "href": "/customers/1/branches"
                    },
                    "invoices": {
                        "href": "/customers/1/invoices"
                    },
                    "paid-invoices": {
                        "href": "/customers/1/invoices/paid"
                    },
                    "pending-invoices": {
                        "href": "/customers/1/invoices/pending"
                    },
                    "overdue-invoices": {
                        "href": "/customers/1/invoices/overdue"
                    }
                }
            }
        ]
    },
    "_links": {
        "self": {
            "href": "http://localhost:8080/data/api/customers"
        }
    },
    "page": {
        "size": 20,
        "total_elements": 1,
        "total_pages": 1,
        "number": 0
    }
}