Skip to content

Add support for specifying array serialization for a particular link relation #811

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
vpavic opened this issue Feb 13, 2019 · 18 comments
Closed
Assignees
Milestone

Comments

@vpavic
Copy link
Contributor

vpavic commented Feb 13, 2019

This is a follow-up on #288, as suggested in last comment on that issue.

We're including item link relation (see https://tools.ietf.org/html/rfc6573#section-2.1) to _links in our HAL responses that return collections/pages.

The code that handles this in @Controller handler method looks something like this:

for (MyResource resource : resources) { // resources is an instance of PagedResources
    resources.add(resource.getLink(Link.REL_SELF).withRel("item"));
}

This works fine when actually there are multiple resource in the collection/page:

"_links": {
    "item": [
        {
            "href": "http://localhost:8080/api/MyResource/1"
        },
        {
            "href": "http://localhost:8080/api/MyResource/2"
        }
    ]
}

However, when there is a single resource in the collection/page, then we don't get an array:

"_links": {
    "item": {
        "href": "http://localhost:8080/api/MyResource/1"
    }
}

Obviously, this isn't ideal from the perspective of client that consumes the service. It would be nice if Spring HATEOAS would provide a support for explicitly specifying array serialization for a certain link relations.

@odrotbohm
Copy link
Member

odrotbohm commented Feb 13, 2019

It looks like HalConfiguration should do the trick already. You should be able to declare it as bean, call ….withRenderSingleLinks(RenderSingleLinks.AS_ARRAY) and HATEOAS should pick this up and consider accordingly.

@vpavic
Copy link
Contributor Author

vpavic commented Feb 14, 2019

For some reason, there seems to be the same misunderstanding as in #288 - this isn't about rendering all link relations as an array, but rather explicitly defined, specific ones.

@gregturn
Copy link
Contributor

We need more details. Is this to be applied for a particular controller? Or a particular domain class?

@vpavic
Copy link
Contributor Author

vpavic commented Feb 14, 2019

Take a look at Section 2.1 from RFC 6573 (I also linked it in the issue description).

Basically we want all our resources to contain item link relations under root _links container. The snippet of the code from the issue description show how we do it - the issue is when there is only a single member in the collection we iterate over.

Ideally, there would be a way to use ResourceSupport#add that would ensure the relation is serialized as an array.

@odrotbohm
Copy link
Member

odrotbohm commented Feb 14, 2019

Maybe I need to clarify: exposing a HalConfiguration bean with ….withRenderSingleLinks(RenderSingleLinks.AS_ARRAY) will make the representation containing a single item link look like this:

"_links": {
    "item": [
        { "href": "http://localhost:8080/api/MyResource/1" }
    ]
}

I.e. even a single link for an particular rel will be rendered as array. Is that not what you were after? You can see this being applied in OptionalListJackson2Serializer.serialize(…).

@vpavic
Copy link
Contributor Author

vpavic commented Feb 14, 2019

I.e. even a single link for an particular rel will be rendered as array. Is that not what you were after?

Yes, but only for a specific link relations, not all.

If we do what you suggest, we'd end up with:

"_links": {
    "first": [
        {
            "href": "http://localhost:8080/api/MyResource?page=1&size=2"
        }
    ],
    "last": [
        {
            "href": "http://localhost:8080/api/MyResource?page=3&size=2"
        }
    ],
    "next": [
        {
            "href": "http://localhost:8080/api/MyResource?page=2&size=2"
        }
    ],
    "item": [
        {
            "href": "http://localhost:8080/api/MyResource/1"
        }
    ],
    "self": [
        {
            "href": "http://localhost:8080/api/MyResource?page=1&size=2"
        }
    ]
},

Which is clearly not desirable. We don't want self, first, last, next relations to be serialized as arrays. We only want that for item relations. To me, that was clear from @vivin's description in #288.

@odrotbohm
Copy link
Member

Oh, I see. While I'd argue that that introduces inconsistency amongst the individual rels, we can certainly extend the configuration object to take a list of strings or patterns to force into arrays.

@odrotbohm odrotbohm changed the title Add support for specifying array serialization for a link relation Add support for specifying array serialization for a particular link relation Feb 14, 2019
@odrotbohm odrotbohm self-assigned this Feb 14, 2019
@odrotbohm odrotbohm added this to the 1.0 M1 milestone Feb 14, 2019
@vpavic
Copy link
Contributor Author

vpavic commented Feb 14, 2019

Thanks for adding this to 1.0.0.M1.

I understand your point about inconsistency between different relations, but IMO that's much less of a concern vs inconsistency between the same relation on different requests. If a certain relation is documented as an array, API consumers should know how to deal with it.

@gregturn
Copy link
Contributor

gregturn commented Feb 14, 2019

If we could fit this into HalConfiguration (compared to annotating controllers or domain objects) it might simpler.

Like

new HalConfiguration()
    .withController(EmployeeController.class, SINGLE_AS_ARRAY)
    .withDomain(Manager.class, SINGLE_AS_ARRAY)

Something like this could signal “for this controller use arrays. And for that domain type do that as well.

With this API, perhaps an annotation based approach could later be cultivated that populates this Wither.

@odrotbohm
Copy link
Member

A rel is not really tied to a controller nor to a domain type. I guess we can provide a way to configure patterns to match, so that you can simplify the setup to configure this based on the curie used.

@gregturn
Copy link
Contributor

I’m still waiting for the use case of why people need this one way in one place and another way elsewhere.

@vpavic
Copy link
Contributor Author

vpavic commented Feb 14, 2019

@gregturn What else is needed, besides all the information already presented here, to clarify the uses for this?

  • some relations, like pagination related ones as the most obvious example, do not make any sense to be serialized as array
  • relations like item as defined in https://tools.ietf.org/html/rfc6573#section-2.1 are OTOH something that naturally fits the serialized as array case.

With that in mind, current situation, where item relation cannot be serialized consistently between subsequent requests is confusing for API consumers:

Concrete examples of current, undesirable situation:

  • page 1 (2 items returned):
$ http :8080/api/MyResource size==2 page==2
HTTP/1.1 200 
Content-Type: application/application/hal+json;charset=UTF-8
Link: <http://localhost:8080/api/MyResource?page=1&size=2>; rel=first
Link: <http://localhost:8080/api/MyResource?page=1&size=2>; rel=prev
Link: <http://localhost:8080/api/MyResource?page=2&size=2>; rel=self
Link: <http://localhost:8080/api/MyResource?page=3&size=2>; rel=next
Link: <http://localhost:8080/api/MyResource?page=3&size=2>; rel=last
Link: <http://localhost:8080/api/MyResource/3>; rel=resources
Link: <http://localhost:8080/api/MyResource/4>; rel=resources

{
    "_embedded": {
        "item": [
            {
                "_links": {
                    "collection": {
                        "href": "http://localhost:8080/api/MyResource"
                    },
                    "self": {
                        "href": "http://localhost:8080/api/MyResource/3"
                    }
                },
                "attribute1": "value1",
                "attribute2": "value2"
            },
            {
                "_links": {
                    "collection": {
                        "href": "http://localhost:8080/api/MyResource"
                    },
                    "self": {
                        "href": "http://localhost:8080/api/MyResource/4"
                    }
                },
                "attribute1": "value1",
                "attribute2": "value2"
            }
        ]
    },
    "_links": {
        "first": {
            "href": "http://localhost:8080/api/MyResource?page=1&size=2"
        },
        "item": [
            {
                "href": "http://localhost:8080/api/MyResource/3"
            },
            {
                "href": "http://localhost:8080/api/MyResource/4"
            }
        ],
        "last": {
            "href": "http://localhost:8080/api/MyResource?page=3&size=2"
        },
        "next": {
            "href": "http://localhost:8080/api/MyResource?page=3&size=2"
        },
        "prev": {
            "href": "http://localhost:8080/api/MyResource?page=1&size=2"
        },
        "self": {
            "href": "http://localhost:8080/api/MyResource?page=2&size=2"
        }
    },
    "page": {
        "number": 2,
        "size": 2,
        "totalElements": 5,
        "totalPages": 3
    }
}
  • page 2 (1 item returned):
$ http :8080/api/MyResource size==2 page==3
HTTP/1.1 200 
Content-Type: application/application/hal+json;charset=UTF-8
Link: <http://localhost:8080/api/MyResource?page=1&size=2>; rel=first
Link: <http://localhost:8080/api/MyResource?page=2&size=2>; rel=prev
Link: <http://localhost:8080/api/MyResource?page=3&size=2>; rel=self
Link: <http://localhost:8080/api/MyResource?page=3&size=2>; rel=last
Link: <http://localhost:8080/api/MyResource/5>; rel=resources

{
    "_embedded": {
        "item": [
            {
                "_links": {
                    "collection": {
                        "href": "http://localhost:8080/api/MyResource"
                    },
                    "self": {
                        "href": "http://localhost:8080/api/MyResource/5"
                    }
                },
                "attribute1": "value1",
                "attribute2": "value2"
            }
        ]
    },
    "_links": {
        "first": {
            "href": "http://localhost:8080/api/MyResource?page=1&size=2"
        },
        "item": {
            "href": "http://localhost:8080/api/MyResource/5"
        },
        "last": {
            "href": "http://localhost:8080/api/MyResource?page=3&size=2"
        },
        "prev": {
            "href": "http://localhost:8080/api/MyResource?page=2&size=2"
        },
        "self": {
            "href": "http://localhost:8080/api/MyResource?page=3&size=2"
        }
    },
    "page": {
        "number": 3,
        "size": 1,
        "totalElements": 5,
        "totalPages": 3
    }
}

@gregturn
Copy link
Contributor

gregturn commented Feb 15, 2019

@vpavic Your example says usage of "item" link relation is the criteria for applying RenderSingleLinks.AS_ARRAY. So to support that, we're talking something like...

new HalConfiguration()
    .renderOverride(IanaLinkRelations.ITEM, RenderSingleLinks.AS_ARRAY)

Then, anywhere item is used, we know to apply this setting. True?

@vpavic
Copy link
Contributor Author

vpavic commented Feb 15, 2019

Yes, that kind of configuration capability would be perfect for our use case.

odrotbohm added a commit that referenced this issue Feb 21, 2019
…ation.

HalConfiguration now exposes a withSingleLinkRenderModeFor(…) taking a path pattern to be rendered in the also given RenderSingleLinks mode. It takes patterns as link relations are either plain strings or valid URIs.

Simplified HAL link list rendering in Jackson2HalModule avoiding double nesting of collections before rendering.
odrotbohm added a commit that referenced this issue Feb 21, 2019
odrotbohm added a commit that referenced this issue Feb 21, 2019
…ation.

HalConfiguration now exposes a withSingleLinkRenderModeFor(…) taking a path pattern to be rendered in the also given RenderSingleLinks mode. It takes patterns as link relations are either plain strings or valid URIs.

Simplified HAL link list rendering in Jackson2HalModule avoiding double nesting of collections before rendering.
odrotbohm added a commit that referenced this issue Feb 21, 2019
@gregturn
Copy link
Contributor

Resolved via aea4c4c.

@vpavic
Copy link
Contributor Author

vpavic commented Mar 29, 2019

@odrotbohm @gregturn Do you have any suggestions on how to work around this on 0.25.x?

I've been poking around on few occasions but haven't been able to find a reasonable way to override logic in Jackson2HalModule.OptionalListJackson2Serializer#serialize. The only (desperate?) idea is to make a custom build of Spring HATEOAS.

@JeffreyDevloo
Copy link

The currently implementation is subawesome. Specifying a relation key which will be rendered as an array across the whole application? This means that you have to define unique links because you don't want to conflict
Why is it not implemented to just chain the link itself?

greeting.add(linkTo(methodOn(GreetingController.class).greeting(name)).withSelfRel().asArray());

with the asArray() just marking a flag on the Link class. That flag can then be read out by the LinkSerializer within the Jackson2HalModule.

@gregturn
Copy link
Contributor

Why don’t you open a new ticket instead of commenting on one that’s been closed and released.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants