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.
- Standard relation names can be found here: http://www.iana.org/assignments/link-relations/link-relations.xhtml
- 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
-
collection-{name} = Related collection of resources {name}. Use singular version of {name}.
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…