toedter / spring-hateoas-jsonapi

A JSON:API media type implementation for Spring HATEOAS
Apache License 2.0
106 stars 15 forks source link

Can't serialize JSON:API model with null ID #60

Closed jschroeder1 closed 2 years ago

jschroeder1 commented 2 years ago

Hi,

I'm attempting to use RestTemplate to Integration Test some endpoints I've set up. I've configured it as specified in the docs and it's mostly working fine but I'm trying to create a resource via a POST route that acceptsapplication/vnd.api+json and I'm running into an issue.

Example:

@Value
@Builder
@JsonApiTypeForClass("testmodels")
public class TestModel {
    @JsonApiId
    private String id;

    private String attribute;
}
    public void test() {
        TestModel testModel = TestModel.builder().attribute("whatever").build();

        final HttpHeaders headers = new HttpHeaders();
        headers.set("Content-Type", MediaTypes.JSON_API_VALUE);

        restTemplate.exchange("/mypostroute",
                HttpMethod.POST,
                new HttpEntity<>(jsonApiModel().model(testModel).build(), headers),
                new TypeReferences.EntityModelType<>() {});
    }

The resource is new and doesn't haven an ID at this point so I would expect it to serialize like this:

{
   "data":{
      "type":"testmodels",
      "attributes":{
         "attribute":"whatever"
      }
   }
}

which I believe should be valid according to the JSON:API spec.

But when I execute the code, it results in an exception:

org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: JSON:API resource object must have property "id".::: TestModel(id=null, attribute=whatever);

If I set id to an empty string, it will serialize successfully.

Is there a way to do this?

toedter commented 2 years ago

Thx for reporting this, I'll take a look at it asap.

toedter commented 2 years ago

You are right: When you want to serialize an EntityModel to JSON:API, an id MUST be present in all cases, except if you do a POST. But if I relax the check to allow serialization without id, your use case would work but in all the other use cases, an invalid JSON:API would be created. This is why the library checks if a valid id can be computed. Since serializing an object to be used for a POST is the exception, I would like to keep the current default behavior since it helps the users to figure out quickly if their Java RepresentationModels can be serialized correctly to JSON:API.

But I understand your point and try to find a configuration option that disables the id check, so that an object without valid id could also be serialized to be used in a POST request.

One way of implementing this could be to configure a marker id value like ignoreJsonApiId or -1 to indicate that the id should not be serialized at all. So

TestModel.builder().id("ignoreJsonApiId").attribute("whatever").build();

would the be serialized without id.

But let me thing a bit longer about it, may be I find a better solution.

Please stay tuned...

toedter commented 2 years ago

You can now add configuration like

new JsonApiConfiguration().withJsonApiIdNotSerializedForValue("ignoreJsonApiId")

Then the above example will be serializes as

{
   "data":{
      "type":"testmodels",
      "attributes":{
         "attribute":"whatever"
      }
   }
}
jschroeder1 commented 2 years ago

@toedter Thank you! Looks like exactly what I need.

I tried it out and I'm able to build the jsonApiModel but when I attempt to POST with RestTemplate, I'm hitting a slightly different exception. Here's what I've got:

In my @Configuration

        @Bean
        public JsonApiMediaTypeConfiguration jsonApiMediaTypeConfiguration(
                ObjectProvider<JsonApiConfiguration> configuration,
                AutowireCapableBeanFactory beanFactory) {
            return new JsonApiMediaTypeConfiguration(configuration, beanFactory);
        }

        @Bean
        public JsonApiConfiguration jsonApiConfiguration() {
            return new JsonApiConfiguration().withJsonApiIdNotSerializedForValue("ignoreJsonApiId");
        }

in the test class:

        TestModel testModel = TestModel.builder().attribute("whatever").build();

        final HttpHeaders headers = new HttpHeaders();
        headers.set("Content-Type", MediaTypes.JSON_API_VALUE);

        RepresentationModel<?> test = jsonApiModel().model(testModel).build();

        restTemplate.exchange("/mypostroute",
                HttpMethod.POST,
                new HttpEntity<>(test, headers),
                new TypeReferences.EntityModelType<>() {});

which now results in an IllegalStateException instead of an HttpMessageNotWritableException:

Caused by: java.lang.IllegalStateException: Cannot compute JSON:API resource id.::: TestModel(id=null, attribute=whatever)
    at com.toedter.spring.hateoas.jsonapi.JsonApiResourceIdentifier.getResourceField(JsonApiResourceIdentifier.java:189)
    at com.toedter.spring.hateoas.jsonapi.JsonApiResourceIdentifier.getId(JsonApiResourceIdentifier.java:92)
    at com.toedter.spring.hateoas.jsonapi.JsonApiData.extractContent(JsonApiData.java:158)
    at com.toedter.spring.hateoas.jsonapi.AbstractJsonApiModelSerializer.serialize(AbstractJsonApiModelSerializer.java:96)
    at com.toedter.spring.hateoas.jsonapi.AbstractJsonApiModelSerializer.serialize(AbstractJsonApiModelSerializer.java:35)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
    ... 80 more

Does that look correct to you or is there something I should be doing differently?

Thanks again.

toedter commented 2 years ago

This looks correct, since the id of your test model cannot be null. If you want the id to be ignored it must exactlxy match the value you specified in the configuration. So, If you initialize you testModel like

TestModel testModel = TestModel.builder().id("ignoreJsonApiId").attribute("whatever").build();

it should serialize without id. Be aware that you can chose the value that is checked for yourself, e.g.

JsonApiConfiguration().withJsonApiIdNotSerializedForValue("-1");
... 
TestModel testModel = TestModel.builder().id("-1").attribute("whatever").build();

would also work.

jschroeder1 commented 2 years ago

Oh right, of course. Missed that part. It's working perfectly now.

Thanks!

toedter commented 2 years ago

I just released version 1.5.0 including this.