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…