Allow for additional links and support for link objects
cruikshj opened this issue · 5 comments
Is your feature request related to a problem? Please describe.
There is no direct way to add additional links to responses. All of the "Link" objects such as TopLevelLinks and ResourceLinks have predetermined properties. No obvious extension point exists to add additional links or respond with link objects.
The spec allows for Link Objects as well as states that the rel can be any valid relation type.
For my use case, I would like to be able to dynamically provide additional links for referencing the operations that are available for an entity, applying context to determine what links should be provided (context such as user permissions). Additionally, I would like to use link objects with describedBy fields per link.
Describe the solution you'd like
An extension point for defining additional (or even overriding default) links. The link value should support both string and link object per spec.
Describe alternatives you've considered
I have considered overriding the serialization classes as a last ditch hook to provide additional links. While this may work, it requires copying sealed classes and reimplementing them with the new link logic.
Additional context
https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs
https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs#L253
Thanks for your consideration
I am considering it.
As for a design, I am thinking about adding a IDictionary<string, LinkObject> Links property to ErrorLinks, ResourceLinks and TopLevelLinks classes, or even base class these classes as an IDictionary<string, LinkObject>. LinkObject could serialize as a string or object per the spec, maybe depending on what properties are set, or a bool override indicating to serialize to object. I would like to modify the existing properties to be backed by the dictionary. Finally, I would modify the serialization classes to handle the dictionary as expected.
This would allow ILinkBuilder or other existing extension points to add additional links.
Does this rough design make sense?
That doesn't look right. We'd want to preserve the link names, such as "self", "related", etc. The spec defines that a link has one of two possible shapes: a string, or an object with specific properties (not a free-format shape you can put anything in). Defining multiple possible shapes is similar to data in the spec, which can be an object or an array.
So this could be modeled as:
public sealed class LinkObject
{
public string Href { get; set; } = null!;
public string? Rel { get; set; }
public string? Describedby { get; set; }
public string? Title { get; set; }
public string? Type { get; set; }
public string? Hreflang { get; set; }
public IDictionary<string, object?>? Meta { get; set; }
}
public struct StringOrLinkObject
{
public string? Value { get; set; }
public LinkObject? LinkObject { get; set; }
}
public sealed class LinksInRelationship
{
public StringOrLinkObject Self { get; set; }
public StringOrLinkObject Related { get; set; }
}The big difference is that for data, the resource graph tells us which shape is needed at any time, so this ugly structure doesn't surface in the JSON. If we'd use the structure above, any link can be a string or an object, and worse: it may vary per request. This means we can't ever possibly know which one applies, so we can't ever expose the proper structure in OpenAPI.
I've tried feeding the following OpenAPI schema to NSwag:
"nullableToOneTeacherInResponse": {
"type": "object",
"properties": {
"links": {
"allOf": [
{
"$ref": "#/components/schemas/linksInRelationship"
}
]
}
},
"additionalProperties": false
},
"linksInRelationship": {
"type": "object",
"properties": {
"self": {
// how it should be: two possible shapes
"oneOf": [
{
"type": "string"
},
{
"$ref": "#/components/schemas/linkObject"
}
]
},
"related": {
"allOf": [
{
// workaround that exposes the ugliness
"$ref": "#/components/schemas/stringOrLinkObject"
}
]
}
},
"additionalProperties": false
},
"stringOrLinkObject": {
"type": "object",
"properties": {
"value": {
"type": "string",
"nullable": true
},
"linkObject": {
"allOf": [
{
"$ref": "#/components/schemas/linkObject"
}
],
"nullable": true
}
},
"additionalProperties": false
},
"linkObject": {
"type": "object",
"properties": {
"href": {
"type": "string"
},
"rel": {
"type": "string",
"nullable": true
},
"describedby": {
"type": "string",
"nullable": true
},
"title": {
"type": "string",
"nullable": true
},
"type": {
"type": "string",
"nullable": true
},
"hreflang": {
"type": "string",
"nullable": true
},
"meta": {
"type": "object",
"additionalProperties": { },
"nullable": true
}
},
"additionalProperties": false
}The self link is how it should be modeled: choose one of two types. But NSwag doesn't support that and simply picks the first entry (so it generates a string property, ignoring the second shape). Not surprising, because C# has no union types. The related link is the next best thing. It works, but it looks quite ugly:
Getting back to your use case: The way I understand the spec, adding custom links is not permitted:
A link’s context is the top-level object, resource object, or relationship object in which it appears.
I don't want to break spec compatibility, because many clients in various programming languages need to be able to consume it. If one of them validates the JSON response against a schema, it would reject the response.
There's an escape hatch though: defining a custom JSON:API profile or extension.
The next best thing is to put the extra links in meta, which is fully free-format.
Thanks for the feedback and for the prototyping of the idea.
The specific link sections do seem to be more prescriptive than the LinkObject section of the spec and I defer to your interpretation of the spec generally. I definitely agree you should not break the spec.
I did intend to use an extension and/or profile to add the additional rel values I was planning.
LinkObject support may still be worth considering but I understand the OpenAPI challenge there.
I will explore the meta idea further, since I am using it for other UI hints already, or possibly a dynamic describedBy.
I will close this issue, but let me know if you would like to me to open a separate issue just for the LinkObject support discussion.
