Recommendation for Non Standard (IANA) Relation Names


#1

Thoughts on a New Recommendation for JSON API

I was thinking about proposing a new “recommendation” for the json:api Recommendations section of the json:api website for non-standard relation names based on naming standards I created at work for our Level 3 REST API where I work. I wanted to propose it here and see what others thought, etc.


Full disclosure - I am the lead software engineer for our “REST API” (WebApi via C#) at www.movietickets.com that powers our regular website, tools, mobile applications, etc. The examples I will be using are from our REST API that is fully json:api compliant.


The following is the naming convention I have instituted when creating relation names for resources in our REST API at movietickets.com

Standards for Relation Names

  • When the purpose of the relation name matches one of the standard types use that name.
  • If none of the registered names match, define an extended relation name using the following conventions:
    • collection-{name} = Related collection of resources {name}. Use singular version of {name}.
      • Example: collection-order
    • resource-{name} = Related individual resource {name}.
      • Example: resource-payment
    • action-{name} = Related action to execute {name}.
      • Example: action-send-email
    • singleton-{name} = Related singleton resource {name}.
      • Example: singleton-search

Note: The “action” and “singleton” conventions are experimental.

What I like about these conventions:

  • Enhances human readability by immediately knowing if the related resources are “to-one” if prefixed with “resource” or are “to-many” if prefixed with “collection”
  • It avoids trying to come up plural versions of names as “collection” literally means “plural”.

A picture is worth a 1024 words so here are some examples to bring this recommendation home:

Here is the json:api entry point document:

{
  "links": {
    "self": "http://localhost:8040"
  },
  "data": {
    "type": null,
    "id": null,
    "attributes": {
      "message": "Entry point into the MovieTickets REST API.",
      "apiVersion": {
        "majorNumber": 2,
        "minorNumber": 0,
        "buildNumber": 100
      },
      "website": "http://www.movietickets.com",
      "mobileWebsite": "http://mobile.movietickets.com"
    },
    "relationships": null,
    "links": {
      "self": "http://localhost:8040",
      "collection-auditorium": "http://localhost:8040/auditoriums",
      "collection-auditorium-attribute": "http://localhost:8040/auditorium-attributes",
      "collection-business-rule": "http://localhost:8040/business-rules",
      "collection-content": "http://localhost:8040/contents",
      "collection-country": "http://localhost:8040/countries",
      "collection-customer": "http://localhost:8040/customers",
      "collection-job": "http://localhost:8040/jobs",
      "collection-order": "http://localhost:8040/orders",
      "collection-layout": "http://localhost:8040/layouts",
      "collection-performance": "http://localhost:8040/performances",
      "collection-seating-type": "http://localhost:8040/seating-types",
      "collection-seat-status": "http://localhost:8040/seat-statuses",
      "collection-seat-type": "http://localhost:8040/seat-types",
      "collection-section-attribute": "http://localhost:8040/section-attributes",
      "collection-theater": "http://localhost:8040/theaters",
      "collection-theater-status": "http://localhost:8040/theater-statuses",
      "collection-zone-attribute": "http://localhost:8040/zone-attributes"
    }
  },
  "included": null
}

Here is the json:api document for an individual performance:

{
  "links": {
    "up": "http://localhost:8040/performances",
    "self": "http://localhost:8040/performances/808683677"
  },
  "data": {
    "type": "performances",
    "id": "808683677",
    "attributes": {
      "scheduleDate": "2015-07-24T00:00:00-05:00",
      "performanceDateTime": "2015-07-24T12:15:00-05:00",
      "isReservedSeating": true,
      "version": null
    },
    "relationships": {
      "resource-auditorium": {
        "links": {
          "related": "http://localhost:8040/performances/808683677/auditorium"
        },
        "data": null,
        "meta": {
          "type": "ToOne"
        }
      },
      "resource-layout": {
        "links": {
          "related": "http://localhost:8040/performances/808683677/layout"
        },
        "data": null,
        "meta": {
          "type": "ToOne"
        }
      },
      "resource-theater": {
        "links": {
          "related": "http://localhost:8040/performances/808683677/theater"
        },
        "data": null,
        "meta": {
          "type": "ToOne"
        }
      },
      "collection-business-rule": {
        "links": {
          "related": "http://localhost:8040/performances/808683677/business-rules"
        },
        "data": null,
        "meta": {
          "type": "ToMany"
        }
      }
    },
    "links": {
      "self": "http://localhost:8040/performances/808683677"
    }
  },
  "included": null
}

Here is the json:api document of an individual layout for reserved seating that has just about everything “including” resource linkage of related resources (note I removed some of the “seats” to reduce the size for clarity purposes):

{
  "links": {
    "up": "http://localhost:8040/layouts",
    "self": "http://localhost:8040/layouts/1192927"
  },
  "data": {
    "type": "layouts",
    "id": "1192927",
    "attributes": {
      "rowCount": 8,
      "columnCount": 12,
      "rows": [
        {
          "top": 0,
          "name": "A"
        },
        {
          "top": 1,
          "name": "B"
        },
        {
          "top": 2,
          "name": "C"
        },
        {
          "top": 3,
          "name": null
        },
        {
          "top": 4,
          "name": "D"
        },
        {
          "top": 5,
          "name": "E"
        },
        {
          "top": 6,
          "name": "F"
        },
        {
          "top": 7,
          "name": "G"
        }
      ],
      "columns": [
        {
          "left": 0,
          "name": null
        },
        {
          "left": 1,
          "name": null
        },
        {
          "left": 2,
          "name": null
        },
        {
          "left": 3,
          "name": null
        },
        {
          "left": 4,
          "name": null
        },
        {
          "left": 5,
          "name": null
        },
        {
          "left": 6,
          "name": null
        },
        {
          "left": 7,
          "name": null
        },
        {
          "left": 8,
          "name": null
        },
        {
          "left": 9,
          "name": null
        },
        {
          "left": 10,
          "name": null
        },
        {
          "left": 11,
          "name": null
        }
      ],
      "sections": [
        {
          "type": "sections",
          "id": "1192927-898-46-0",
          "attributes": {
            "name": "Reserved Seating",
            "top": 0,
            "left": 0,
            "tags": [],
            "version": null
          },
          "relationships": {
            "resource-seating-type": {
              "links": null,
              "data": {
                "type": "seating-types",
                "id": "SelectASeat"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "collection-seat": {
              "links": {
                "related": "http://localhost:8040/layouts/1192927/sections/1192927-898-46-0/seats"
              },
              "data": [
                {
                  "type": "seats",
                  "id": "1192927-898-46-0-1-1"
                },
                {
                  "type": "seats",
                  "id": "1192927-898-46-0-1-2"
                },
                {
                  "type": "seats",
                  "id": "1192927-898-46-0-1-3"
                },
                {
                  "type": "seats",
                  "id": "1192927-898-46-0-1-4"
                },
                {
                  "type": "seats",
                  "id": "1192927-898-46-0-8-12"
                }
              ],
              "meta": {
                "type": "ToMany"
              }
            }
          },
          "links": {
            "self": "http://localhost:8040/layouts/1192927/sections/1192927-898-46-0"
          }
        }
      ],
      "zones": [],
      "seats": [
        {
          "type": "seats",
          "id": "1192927-898-46-0-1-1",
          "attributes": {
            "top": 0,
            "left": 0,
            "rowName": "A",
            "seatName": "12",
            "seatGroup": null,
            "version": null
          },
          "relationships": {
            "resource-section": {
              "links": null,
              "data": {
                "type": "sections",
                "id": "1192927-898-46-0"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seating-type": {
              "links": null,
              "data": {
                "type": "seating-types",
                "id": "SelectASeat"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seat-type": {
              "links": null,
              "data": {
                "type": "seat-types",
                "id": "Normal"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seat-status": {
              "links": null,
              "data": {
                "type": "seat-statuses",
                "id": "Unknown"
              },
              "meta": {
                "type": "ToOne"
              }
            }
          },
          "links": {
            "self": "http://localhost:8040/layouts/1192927/seats/1192927-898-46-0-1-1"
          }
        },
        {
          "type": "seats",
          "id": "1192927-898-46-0-1-2",
          "attributes": {
            "top": 0,
            "left": 1,
            "rowName": "A",
            "seatName": "11",
            "seatGroup": null,
            "version": null
          },
          "relationships": {
            "resource-section": {
              "links": null,
              "data": {
                "type": "sections",
                "id": "1192927-898-46-0"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seating-type": {
              "links": null,
              "data": {
                "type": "seating-types",
                "id": "SelectASeat"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seat-type": {
              "links": null,
              "data": {
                "type": "seat-types",
                "id": "Normal"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seat-status": {
              "links": null,
              "data": {
                "type": "seat-statuses",
                "id": "Unknown"
              },
              "meta": {
                "type": "ToOne"
              }
            }
          },
          "links": {
            "self": "http://localhost:8040/layouts/1192927/seats/1192927-898-46-0-1-2"
          }
        },
        {
          "type": "seats",
          "id": "1192927-898-46-0-1-3",
          "attributes": {
            "top": 0,
            "left": 2,
            "rowName": "A",
            "seatName": "10",
            "seatGroup": null,
            "version": null
          },
          "relationships": {
            "resource-section": {
              "links": null,
              "data": {
                "type": "sections",
                "id": "1192927-898-46-0"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seating-type": {
              "links": null,
              "data": {
                "type": "seating-types",
                "id": "SelectASeat"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seat-type": {
              "links": null,
              "data": {
                "type": "seat-types",
                "id": "Normal"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seat-status": {
              "links": null,
              "data": {
                "type": "seat-statuses",
                "id": "Unknown"
              },
              "meta": {
                "type": "ToOne"
              }
            }
          },
          "links": {
            "self": "http://localhost:8040/layouts/1192927/seats/1192927-898-46-0-1-3"
          }
        },
        {
          "type": "seats",
          "id": "1192927-898-46-0-1-4",
          "attributes": {
            "top": 0,
            "left": 3,
            "rowName": "A",
            "seatName": "9",
            "seatGroup": null,
            "version": null
          },
          "relationships": {
            "resource-section": {
              "links": null,
              "data": {
                "type": "sections",
                "id": "1192927-898-46-0"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seating-type": {
              "links": null,
              "data": {
                "type": "seating-types",
                "id": "SelectASeat"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seat-type": {
              "links": null,
              "data": {
                "type": "seat-types",
                "id": "Normal"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seat-status": {
              "links": null,
              "data": {
                "type": "seat-statuses",
                "id": "Unknown"
              },
              "meta": {
                "type": "ToOne"
              }
            }
          },
          "links": {
            "self": "http://localhost:8040/layouts/1192927/seats/1192927-898-46-0-1-4"
          }
        },
        {
          "type": "seats",
          "id": "1192927-898-46-0-8-12",
          "attributes": {
            "top": 7,
            "left": 11,
            "rowName": "G",
            "seatName": "1",
            "seatGroup": null,
            "version": null
          },
          "relationships": {
            "resource-section": {
              "links": null,
              "data": {
                "type": "sections",
                "id": "1192927-898-46-0"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seating-type": {
              "links": null,
              "data": {
                "type": "seating-types",
                "id": "SelectASeat"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seat-type": {
              "links": null,
              "data": {
                "type": "seat-types",
                "id": "Normal"
              },
              "meta": {
                "type": "ToOne"
              }
            },
            "resource-seat-status": {
              "links": null,
              "data": {
                "type": "seat-statuses",
                "id": "Unknown"
              },
              "meta": {
                "type": "ToOne"
              }
            }
          },
          "links": {
            "self": "http://localhost:8040/layouts/1192927/seats/1192927-898-46-0-8-12"
          }
        }
      ],
      "version": null
    },
    "relationships": {
      "collection-section": {
        "links": {
          "related": "http://localhost:8040/layouts/1192927/sections"
        },
        "data": null,
        "meta": {
          "type": "ToMany"
        }
      },
      "collection-seat": {
        "links": {
          "related": "http://localhost:8040/layouts/1192927/seats"
        },
        "data": null,
        "meta": {
          "type": "ToMany"
        }
      }
    },
    "links": {
      "self": "http://localhost:8040/layouts/1192927"
    }
  },
  "included": [
    {
      "type": "seat-statuses",
      "id": "Unknown",
      "attributes": {
        "name": "Unknown",
        "isSeatReservable": false
      },
      "relationships": null,
      "links": {
        "self": "http://localhost:8040/seat-statuses/Unknown"
      }
    },
    {
      "type": "seat-types",
      "id": "HandicapAccessible",
      "attributes": {
        "name": "Handicap Accessible",
        "iconUrl": "http://www.movietickets.com/images/access-seats-sprite.svg",
        "isSeatDrawable": true,
        "isSeatHandicapAccessible": true,
        "isSeatHandicapCompanion": false,
        "isSeatReservable": true
      },
      "relationships": null,
      "links": {
        "self": "http://localhost:8040/seat-types/HandicapAccessible"
      }
    },
    {
      "type": "seat-types",
      "id": "None",
      "attributes": {
        "name": "None",
        "iconUrl": null,
        "isSeatDrawable": false,
        "isSeatHandicapAccessible": false,
        "isSeatHandicapCompanion": false,
        "isSeatReservable": false
      },
      "relationships": null,
      "links": {
        "self": "http://localhost:8040/seat-types/None"
      }
    },
    {
      "type": "seat-types",
      "id": "Normal",
      "attributes": {
        "name": "Normal",
        "iconUrl": "http://www.movietickets.com/images/avail-seats-sprite.svg",
        "isSeatDrawable": true,
        "isSeatHandicapAccessible": false,
        "isSeatHandicapCompanion": false,
        "isSeatReservable": true
      },
      "relationships": null,
      "links": {
        "self": "http://localhost:8040/seat-types/Normal"
      }
    },
    {
      "type": "seating-types",
      "id": "SelectASeat",
      "attributes": {
        "name": "Select-A-Seat",
        "isSeatSelectable": true
      },
      "relationships": null,
      "links": {
        "self": "http://localhost:8040/seating-types/SelectASeat"
      }
    }
  ]
}

Thoughts and/or opinions on this recommendation…


#2

I certainly agree with using standard relations whenver possible. I’m not totally sure about these prefixes, i’ll have to give it some more thought…


#3

I have been kicking this around for a while, and while I like most of what you have there, the format and prefixes but I wonder about their discoverability. How has this format worked out, have the action and singleton prefixes been relatively painless? I really do like how you have added the toOne or toMany to the meta property of the link, to me that would be the better way to address the cardinality of the link.

The format I am tossing around is similar, though is a bit more delimited and concerned with stronger global semantics. (Please don’t read that as semantic web.) Here is what I have.

(Semantic Operation;)(Namespace; | Curie;){name}

In this format I believe you could handle all of the the concerns you have mentioned, without having to worry about repeating yourself, or adding additional consumer requirements for processing.

edit;http://example.org;address

or with the assume support of curies in extensions where ex is the curie for http://example.org.

edit;ex;address

This has the benefit of being extremely lean, but completely declarative and context dependent. All of which should reduce the complexity of supporting a hypermedia style api.


#4

@michaelhibay Here is the epilogue on this recommendation. Yes we did adopt this naming standard for non-standard relation names for relationships in the beginning of the development of our json:api based hypermedia API. Yes it did help in developer readability and spelling for that matter, in the end we actually regret adopting this standard and would not recommend it. When we started adding things like filter and include query parameters the query parameters became cumbersome and arduous to create and work with.

I would recommend relation names like “author” and “comments” over “resource-author” and “collection-comments”. In our next major version of the json:api based hypermedia API we are going to this style of relationship relation names.


#5

My theory was to try to stick as close to the standard relation names as possible, as they are universally understood with the curie or namespace and resource name as meta information which the client can use to prefetch or fetch the profile, and documentation of the resource.

I overloaded the rel name for this meta information because I really would rather shy away from the non standard catch all container for ‘other’ stuff unless it is overly burdensome or cognitively dissonant.

However, its possible a profile of meta information regarding rel names could be added to the meta block as a pseudo standard until such a time as it can be worked through an extension or into the base spec.

Could you elaborate on why filter and include query params proved troublesome to your implementation? I am having difficulty imagining how the relation names would cause issues with creating query params.


#6

Let me paraphrase, the original intent of this recommendation was a naming strategy for the “rels” used in relationships between resources which are non-standard. For the json:api specification you would always use the IANA standard “rels” in document and resource links for things like up, self, related, first, next, prev.

The actual naming strategy we used was the following:

  • “collection-{name}” for a “to-many” relationship: article has many comments, therefore the “rel” was “collection-comment” in the article relationships.
  • “resource-{name}” for a “to-one” relationship: article has one author, therefore the “rel” was “resource-author” in the article relationships.
  • We never did use the “action-{name}” or “singleton-{name}” naming standards.

The reason we do not like these prefixed relationship names was because we use the browser as pragmatic development tool for navigation and exercising GET requests in the hypermedia API with filtering, inclusion, etc. When I said these naming standards were cumbersome and arduous to create and work with I meant as a human typing out the much longer “rel” names for things like filtering on related resource attributes or including related resources in the json:api document in the address bar of the browser. Also when reading and documenting various filters, sorts, or inclusions for popular queries, these prefixed names at times obfuscated the query in my opinion.

So in the end the original intention was honorable, but we now currently do not like it for the pragmatic reasons I just mentioned.


#7

I completely follow and understand your use of the standard IANA relations from the JsonApi specification standpoint, the disconnect to me appears to be how the browser validation would include manually typing out those rel names within queries.

From my perspective, the rel is a known action / item identifier within a vocabulary, from which you would pull the URI value, and filtering and inclusion parameters would end up being URI value + query params. So I am finding it hard to understand why the particular value of the key to the k,v pair would make it any more difficult to work with when the primary concern is obtaining the value.

Did your custom rel links already include things like query parameters, or default inclusion which would require additional work to parse and add the new parameters?

As an additional somewhat query, I see in your example you included toMany or toOne in addition to the collection- or resource- within the rel name. Did you continue to use this within your approach, or did this functionality get deprecated along with the action and singleton prefixes?


#8

To answer your first question, when I say “difficult” to work with all I meant was physically typing out the request in the address bar as a developer in addition to human readability obfuscation. Let me give you an actual real world example from our actual hypermedia API that finds a theater resource based on a syndicator identifier and includes the theater status as just a small example, which one of these would your prefer as a developer or client of the hypermedia API? In the end we prefer V3 so our next version will use this “style”…

Current Version V2 with this recommendation

https://api.example.com/v2/en-us/theaters?filter=collection-theater-syndication.resource-syndicator.IsWestWorldMedia eq true and collection-theater-syndication.SyndicationId eq 8487&include=resource-theater-status

Next Version V3 without this recommendation

https://api.example.com/v3/en-us/theaters?filter=theater-syndications.syndicator.IsWestWorldMedia eq true and theater-syndications.SyndicationId eq 8487&include=theater-status

To answer your second question we no longer include the relationship cardinality in meta of the relationship as it is not needed for the following reasons:

  1. We have documented the “domain model” of our hypermedia API for clients which includes the relationships and their respective “rels” to use when getting related resources.
  2. You can infer the cardinality when resource linkage is present:
    2.1 If data is null or a resource identifier => to-one
    2.2 If data is an array of resource identifiers => to-many

#9

Now that makes sense. I had gone through your other post about the use of ODATA queries, but if you’re directly using the rels within the query parameters in that way, it would be certainly be a pain. Did this prove to be an issue in the client, integration or interoperation aspects, or simply just a huge annoyance when manually invoking certain calls?

It seems you came to the same conclusion about cardinality as I did in my previous post, so your relationship meta scheme looks very similar to the standard domain vocabulary model. However, I’m still on the fence I think regarding including curie / namespace and the target resource name within the relationship name. On one hand it could prove extremely helpful to a client trying to utilize the rel. However it could be welcoming a certain amount of snowflake qualities bank into the application, and clearly this would be an undesirable outcome. It might just be more beneficial to define the vocabulary and utilize the other benefits of hypermedia to fill in the information gaps.