Json API


JSON-API is a specification for building REST APIs for CRUD (create, read, update, and delete) operations.
Similar to GraphQL:

  • It allows the client to control what is returned in the response payload.
  • It provided an API extension (the patch extension that allowed multiple mutations to the graph to occur in a single request.

Unlike GraphQL, the JSON-API specification spells out exactly how to perform common CRUD operations including complex graph mutations.
JSON-API has no standardized schema introspection. However, Elide adds this capability to any service by exporting an Open API Initiative document (formerly known as Swagger).

The json-api specification is the best reference for understanding JSON-API. The following sections describe commonly used JSON-API features as well as Elide additions for filtering, pagination, sorting, and swagger.

Hierarchical URLs


Elide generally follows the JSON-API recommendations for URL design.

There are a few caveats given that Elide allows developers control over how entities are exposed:

  1. Some entities may only be reached through a relationship to another entity. Not every entity is rootable.
  2. The root path segment of URLs are by default the name of the class (lowercase). This can be overridden.
  3. Elide allows relationships to be nested arbitrarily deep in URLs.
  4. Elide currently requires all individual entities to be addressed by ID within a URL. For example, consider a model with an article and a singular author which has a singular address. While unambiguous, the following is not allowed: /articles/1/author/address.
    Instead, the author must be fully qualified by ID: /articles/1/author/34/address

Model Identifiers


Elide supports three mechanisms by which a newly created entity is assigned an ID:

  1. The ID is assigned by the client and saved in the data store.
  2. The client doesn’t provide an ID and the data store generates one.
  3. The client provides an ID which is replaced by one generated by the data store. When using the patch extension, the client must provide an ID to identify objects which are both created and added to collections in other objects. However, in some instances the server should have ultimate control over the ID that is assigned.

Elide looks for the JPA GeneratedValue annotation to disambiguate whether or not the data store generates an ID for a given data model. If the client also generated an ID during the object creation request, the data store ID overrides the client value.

Matching newly created objects to IDs

When using the patch extension, Elide returns object entity bodies (containing newly assigned IDs) in the order in which they were created. The client can use this order to map the object created to its server assigned ID.

Sparse Fields


JSON-API allows the client to limit the attributes and relationships that should be included in the response payload for any given entity. The fields query parameter specifies the type (data model) and list of fields that should be included.

For example, to fetch the book collection but only include the book titles:

/book?fields[book]=title
{
  "data": [
      {
          "attributes": {
              "title": "The Old Man and the Sea"
          },
          "id": "1",
          "type": "book"
      },
      {
          "attributes": {
              "title": "For Whom the Bell Tolls"
          },
          "id": "2",
          "type": "book"
      },
      {
          "attributes": {
              "title": "Enders Game"
          },
          "id": "3",
          "type": "book"
      }
  ]
}

More information about sparse fields can be found here.

Compound Documents


JSON-API allows the client to fetch a primary collection of elements but also include their relationships or their relationship’s relationships (arbitrarily nested) through compound documents. The include query parameter specifies what relationships should be expanded in the document.

The following example fetches the book collection but also includes all of the book authors. Sparse fields are used to limit the book and author fields in the response:

/book?include=authors&fields[book]=title,authors&fields[author]=name
{
  "data": [
      {
          "attributes": {
              "title": "The Old Man and the Sea"
          },
          "id": "1",
          "relationships": {
              "authors": {
                  "data": [
                      {
                          "id": "1",
                          "type": "author"
                      }
                  ]
              }
          },
          "type": "book"
      },
      {
          "attributes": {
              "title": "For Whom the Bell Tolls"
          },
          "id": "2",
          "relationships": {
              "authors": {
                  "data": [
                      {
                          "id": "1",
                          "type": "author"
                      }
                  ]
              }
          },
          "type": "book"
      },
      {
          "attributes": {
              "title": "Enders Game"
          },
          "id": "3",
          "relationships": {
              "authors": {
                  "data": [
                      {
                          "id": "2",
                          "type": "author"
                      }
                  ]
              }
          },
          "type": "book"
      }    
  ],
  "included": [
      {
          "attributes": {
              "name": "Ernest Hemingway"
          },
          "id": "1",
          "type": "author"
      },
      {
          "attributes": {
              "name": "Orson Scott Card"
          },
          "id": "2",
          "type": "author"
      }    
  ]
}

More information about compound documents can be found here.

Filtering


JSON-API 1.0 is agnostic to filtering strategies. The only recommendation is that servers and clients should prefix filtering query parameters with the word ‘filter’.

Elide supports multiple filter dialects and the ability to add new ones to meet the needs of developers or to evolve the platform should JSON-API standardize them. Elide’s primary dialect is RSQL

RSQL

RSQL is a query language that allows conjunction (and), disjunction (or), and parenthetic grouping of Boolean expressions. It is a superset of the FIQL language.

Because RSQL is a superset of FIQL, FIQL queries should be properly parsed. RSQL primarily adds more friendly lexer tokens to FIQL for conjunction and disjunction: ‘and’ instead of ‘;’ and ‘or’ instead of ‘,’. RSQL also adds a richer set of operators.

Filter Syntax

Filter query parameters look like filter[TYPE] where ‘TYPE’ is the name of the data model/entity.
Any number of filter parameters can be specified provided the ‘TYPE’ is different for each parameter.

The value of any query parameter is a RSQL expression composed of predicates. Each predicate contains an attribute of the data model, an operator, and zero or more comparison values.

Filter Examples

Return all the books written by author ‘1’ with the genre exactly equal to ‘Science Fiction’:

/author/1/book?filter[book]=genre=='Science Fiction'

Return all the books written by author ‘1’ with the genre exactly equal to ‘Science Fiction’ and the title starts with ‘The’:

/author/1/book?filter[book]=genre=='Science Fiction';title==The*

Return all the books written by author ‘1’ with the publication date greater than a certain time or the genre not ‘Literary Fiction’ or ‘Science Fiction’:

/author/1/book?filter[book]=publishDate>1454638927411,genre=out=('Literary Fiction','Science Fiction')

Return all the books whose title contains ‘Foo’. Include all the authors of those books whose name does not equal ‘Orson Scott Card’:

/book?include=authors&filter[book]=title==*Foo*&filter[author]=name!='Orson Scott Card'

Operators

The following RSQL operators are supported:

  • =in= : Evaluates to true if the attribute exactly matches any of the values in the list.
  • =out= : Evaluates to true if the attribute does not match any of the values in the list.
  • ==ABC* : Similar to SQL like 'ABC%.
  • ==*ABC : Similar to SQL like '%ABC.
  • ==*ABC* : Similar to SQL like '%ABC%.
  • =isnull=true : Evaluates to true if the attribute is null
  • =isnull=false : Evaluates to true if the attribute is not null
  • =lt= : Evaluates to true if the attribute is less than the value.
  • =gt= : Evaluates to true if the attribute is greater than the value.
  • =le= : Evaluates to true if the attribute is less than or equal to the value.
  • =ge= : Evaluates to true if the attribute is greater than or equal to the value.

Values & Type Coercion

Values are specified as URL encoded strings. Elide will type coerce them into the appropriate primitive data type for the attribute filter.

Pagination


Elide supports:

  1. Paginating a collection by row offset and limit.
  2. Paginating a collection by page size and number of pages.
  3. Returning the total size of a collection visible to the given user.
  4. Returning a meta block in the JSON-API response body containing metadata about the collection.
  5. A simple way to control:
    • the availability of metadata
    • the number of records that can be paginated

Syntax

Elide allows pagination of the primary collection being returned in the response via the page query parameter.

The rough BNF syntax for the page query parameter is:

<QUERY> ::= 
     "page" "[" "size" "]" "=" <INTEGER>
   | "page" "[" "number" "]" "=" <INTEGER>
   | "page" "[" "limit" "]" "=" <INTEGER>
   | "page" "[" "offset" "]" "=" <INTEGER>
   | "page" "[" "totals" "]"

Legal combinations of the page query params include:

  1. size
  2. number
  3. size & number
  4. size & number & totals
  5. offset
  6. limit
  7. offset & limit
  8. offset & limit & totals

Meta Block

Whenever a page query parameter is specified, Elide will return a meta block in the JSON-API response that contains:

  1. The page number
  2. The page size or limit
  3. The total number of pages (totalPages) in the collection
  4. The total number of records (totalRecords) in the collection.

The values for totalPages and totalRecords are only returned if the page[totals] parameter was specified in the query.

Example

Paginate the book collection starting at the 4th record. Include no more than 2 books per page. Include the total size of the collection in the meta block:

/book?page[offset]=3&page[limit]=2&page[totals]
{
  "data": [
      {
          "attributes": {
              "chapterCount": 0,
              "editorName": null,
              "genre": "Science Fiction",
              "language": "English",
              "publishDate": 1464638927412,
              "title": "Enders Shadow"
          },
          "id": "4",
          "relationships": {
              "authors": {
                  "data": [
                      {
                          "id": "2",
                          "type": "author"
                      }
                  ]
              },
              "chapters": {
                  "data": []
              },
              "publisher": {
                  "data": null
              }
          },
          "type": "book"
      },
      {
          "attributes": {
              "chapterCount": 0,
              "editorName": null,
              "genre": "Science Fiction",
              "language": "English",
              "publishDate": 0,
              "title": "Foundation"
          },
          "id": "5",
          "relationships": {
              "authors": {
                  "data": [
                      {
                          "id": "3",
                          "type": "author"
                      }
                  ]
              },
              "chapters": {
                  "data": []
              },
              "publisher": {
                  "data": null
              }
          },
          "type": "book"
      }
  ],
  "meta": {
      "page": {
          "limit": 2,
          "number": 2,
          "totalPages": 4,
          "totalRecords": 8
      }
  }
}

Sorting


Elide supports:

  1. Sorting a collection by any attribute of the collection’s type.
  2. Sorting a collection by multiple attributes at the same time in either ascending or descending order.
  3. Sorting a collection by any attribute of a to-one relationship of the collection’s type. Multiple relationships can be traversed provided the path from the collection to the sorting attribute is entirely through to-one relationships.

Syntax

Elide allows sorting of the primary collection being returned in the response via the sort query parameter.

The rough BNF syntax for the sort query parameter is:

<QUERY> ::= "sort" "=" <LIST_OF_SORT_SPECS>

<LIST_OF_SORT_SPECS> = <SORT_SPEC> | <SORT_SPEC> "," <LIST_OF_SORT_SPECS>

<SORT_SPEC> ::= "+|-"? <PATH_TO_ATTRIBUTE>

<PATH_TO_ATTRIBUTE> ::= <RELATIONSHIP> <PATH_TO_ATTRIBUTE> | <ATTRIBUTE>

<RELATIONSHIP> ::= <TERM> "."

<ATTRIBUTE> ::= <TERM>

Sort By ID

The keyword id can be used to sort by whatever field a given entity uses as its identifier.

Example

Sort the collection of author 1’s books in descending order by the book’s publisher’s name:

/author/1/books?sort=-publisher.name
{
  "data": [
      {
          "attributes": {
              "chapterCount": 0,
              "editorName": null,
              "genre": "Literary Fiction",
              "language": "English",
              "publishDate": 0,
              "title": "For Whom the Bell Tolls"
          },
          "id": "2",
          "relationships": {
              "authors": {
                  "data": [
                      {
                          "id": "1",
                          "type": "author"
                      }
                  ]
              },
              "chapters": {
                  "data": []
              },
              "publisher": {
                  "data": {
                      "id": "2",
                      "type": "publisher"
                  }
              }
          },
          "type": "book"
      },
      {
          "attributes": {
              "chapterCount": 0,
              "editorName": null,
              "genre": "Literary Fiction",
              "language": "English",
              "publishDate": 0,
              "title": "The Old Man and the Sea"
          },
          "id": "1",
          "relationships": {
              "authors": {
                  "data": [
                      {
                          "id": "1",
                          "type": "author"
                      }
                  ]
              },
              "chapters": {
                  "data": []
              },
              "publisher": {
                  "data": {
                      "id": "1",
                      "type": "publisher"
                  }
              }
          },
          "type": "book"
      }
  ]
}

Bulk Writes And Complex Mutations


JSON-API supported a now-deprecated mechanism for extensions.
The patch extension was a JSON-API extension that allowed muliple mutation operations (create, delete, update) to be bundled together in as single request.

Elide supports the JSON-API patch extension because it allows complex & bulk edits to the data model in the context of a single transaction. For example, the following request creates an author (earnest hemingway), multiple of his books, and his book publisher in a single request:

[
  {
    "op": "add",
    "path": "/author",
    "value": {
      "id": "12345678-1234-1234-1234-1234567890ab",
      "type": "author",
      "attributes": {
        "name": "Ernest Hemingway"
      },
      "relationships": {
        "books": {
          "data": [
            {
              "type": "book",
              "id": "12345678-1234-1234-1234-1234567890ac"
            },
            {
              "type": "book",
              "id": "12345678-1234-1234-1234-1234567890ad"
            }
          ]
        }
      }
    }
  },
  {
    "op": "add",
    "path": "/book",
    "value": {
      "type": "book",
      "id": "12345678-1234-1234-1234-1234567890ac",
      "attributes": {
        "title": "The Old Man and the Sea",
        "genre": "Literary Fiction",
        "language": "English"
      },
      "relationships": {
        "publisher": {
            "data": {
                "type": "publisher",
                "id": "12345678-1234-1234-1234-1234567890ae"
            }
        }
      }
    }
  },
  {
    "op": "add",
    "path": "/book",
    "value": {
      "type": "book",
      "id": "12345678-1234-1234-1234-1234567890ad",
      "attributes": {
        "title": "For Whom the Bell Tolls",
        "genre": "Literary Fiction",
        "language": "English"
      }
    }
  },
  {
    "op": "add",
    "path": "/book/12345678-1234-1234-1234-1234567890ac/publisher",
    "value": {
        "type": "publisher",
        "id": "12345678-1234-1234-1234-1234567890ae",
        "attributes": {
            "name": "Default publisher"
        }
    }
  }
]
[
  {
    "data": {
      "attributes": {
        "name": "Ernest Hemingway"
      },
      "id": "1",
      "relationships": {
        "books": {
          "data": [
            {
              "id": "1",
              "type": "book"
            },
            {
              "id": "2",
              "type": "book"
            }
          ]
        }
      },
      "type": "author"
    }
  },
  {
    "data": {
      "attributes": {
        "chapterCount": 0,
        "editorName": null,
        "genre": "Literary Fiction",
        "language": "English",
        "publishDate": 0,
        "title": "The Old Man and the Sea"
      },
      "id": "1",
      "relationships": {
        "authors": {
          "data": [
            {
              "id": "1",
              "type": "author"
            }
          ]
        },
        "chapters": {
          "data": []
        },
        "publisher": {
          "data": {
            "id": "1",
            "type": "publisher"
          }
        }
      },
      "type": "book"
    }
  },
  {
    "data": {
      "attributes": {
        "chapterCount": 0,
        "editorName": null,
        "genre": "Literary Fiction",
        "language": "English",
        "publishDate": 0,
        "title": "For Whom the Bell Tolls"
      },
      "id": "2",
      "relationships": {
        "authors": {
          "data": [
            {
              "id": "1",
              "type": "author"
            }
          ]
        },
        "chapters": {
          "data": []
        },
        "publisher": {
          "data": null
        }
      },
      "type": "book"
    }
  },
  {
    "data": {
      "attributes": {
        "name": "Default publisher"
      },
      "id": "1",
      "type": "publisher"
    }
  }
]

Swagger


Swagger documents can be highly customized. As a result, they are not enabled by default and instead must be initialized through code. The steps to do this are documented here.