JSON API
JSON-API is a specification for building REST APIs for CRUD (create, read, update, and delete) operations. Similar to GraphQL, it
- It allows the client to control what is returned in the response payload.
- It provides a mechanism in the form of extensions (the Atomic Operations Extension and JSON Patch Extension) that allows 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 OpenAPI 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 generation of OpenAPI documents.
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:
- Some entities may only be reached through a relationship to another entity. Not every entity is rootable.
- The root path segment of URLs are by default the name of the class (lowercase). This can be overridden.
- Elide allows relationships to be nested arbitrarily deep in URLs.
- 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:
- The ID is assigned by the client and saved in the data store.
- The client doesn't provide an ID and the data store generates one.
- The client provides an ID which is replaced by one generated by the data store. When using the Atomic Operations Extension or JSON Patch Extension, the client must provide an ID or Local 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 Atomic Operations Extension or JSON 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:
- Request
- Response
/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:
- Request
- 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 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. FIQL defines all String comparison operators to be case insensitive. Elide overrides this behavior making all operators case sensitive by default. For case insensitive queries, Elide introduces new operators.
Filter Syntax
Filter query parameters either look like:
filter[TYPE]
where 'TYPE' is the name of the data model/entity. These are type specific filters and only apply to filtering collections of the given type.filter
with no type specified. This is a global filter and can be used to filter across relationships (by performing joins in the persistence layer).
Any number of typed filter parameters can be specified provided the 'TYPE' is different for each parameter. There can only be a single global filter for the entire query. Typed filters can be used for any collection returned by Elide. Global filters can only be used to filter root level collections.
The value of any query parameter is a RSQL expression composed of predicates. Each predicate contains an attribute of the data model or a related data model, an operator, and zero or more comparison values.
Filter attributes can be:
- In the data model itself
- In another related model traversed through to-one or to-many relationships
- Inside an object or nested object hierarchy
To join across relationships or drill into nested objects, the attribute name is prefixed by one or more relationship or field names separated by period ('.'). For example, 'author.books.price.total' references all of the author's books with a price having a particular total value.
Typed 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' and whose total price is greater than 100.00:
/author/1/book?filter[book]=genre=='Science Fiction';title==The*;price.total>100.00
Return all the books written by author '1' with the publication date greater than a certain time or the genre not being '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'
Global Filter Examples
Return all the books with an author whose name is 'Null Ned' and whose title is 'Life with Null Ned':
/book?filter=authors.name=='Null Ned';title=='Life with Null Ned'
Operators
The following RSQL operators are supported:
Operator | Description |
---|---|
=in= | Evaluates to true if the attribute exactly matches any of the values in the list. (Case Sensitive) |
=ini= | Evaluates to true if the attribute exactly matches any of the values in the list. (Case Insensitive) |
=out= | Evaluates to true if the attribute does not match any of the values in the list. (Case Sensitive) |
=outi= | Evaluates to true if the attribute does not match any of the values in the list. (Case Insensitive) |
==ABC* | Similar to SQL like 'ABC%' . (Case Sensitive) |
==*ABC | Similar to SQL like '%ABC' . (Case Sensitive) |
==*ABC* | Similar to SQL like '%ABC%' . (Case Sensitive) |
=ini=ABC* | Similar to SQL like 'ABC%' . (Case Insensitive) |
=ini=*ABC | Similar to SQL like '%ABC' . (Case Insensitive) |
=ini=*ABC* | Similar to SQL like '%ABC%' . (Case Insensitive) |
=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. |
=isempty= | Determines if a collection is empty or not. |
=between= | Determines if a model attribute is >= and <= the two provided arguments. |
=notbetween= | Negates the between operator. |
=hasmember= | Determines if a collection contains a particular element. This can be used to evaluate that an attribute across a to-many association has a null value present by using =hasmember=null . |
=hasnomember= | Determines if a collection does not contain a particular element. |
=subsetof= | Determines if a collection is a subset of the values in the list. Meaning all the elements of the collection are in the provided values. Note that an empty set is a subset of every set. |
=notsubsetof= | Determines if a collection is not a subset of the values in the list. |
=supersetof= | Determines if a collection is a superset of the values in the list. Meaning all the elements in the provided values are in the collection. |
=notsupersetof= | Determines if a collection is not a superset of the values in the list. |
The operators hasmember
, hasnomember
, subsetof
, notsubsetof
, supersetof
, notsupersetof
can be applied to collections (book.awards) or across to-many relationships (book.authors.name).
FIQL Default Behaviour
By default, the FIQL operators =in=
, =out=
, ==
are case sensitive. This can be reverted to case insensitive by
changing the case sensitive strategy:
@Configuration
public class ElideConfiguration {
@Bean
public JsonApiSettingsBuilderCustomizer jsonApiSettingsBuilderCustomizer() {
return builder -> builder
.joinFilterDialect(new RSQLFilterDialect(dictionary), new CaseSensitivityStrategy.FIQLCompliant())
.subqueryFilterDialect(new RSQLFilterDialect(dictionary), new CaseSensitivityStrategy.FIQLCompliant());
}
}
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.
Attribute arguments.
Some data stores like the Aggregation Store support parameterized model attributes. Parameters can be included in a filter predicate with the following syntax:
field[arg1:value1][arg2:value2]
Argument values must be URL encoded. There is no limit to the number of arguments provided in this manner.
Pagination
Elide supports:
- paginating a collection by row offset and limit.
- paginating a collection by page size and number of pages.
- returning the total size of a collection visible to the given user.
- returning a meta block in the JSON-API response body containing metadata about the collection or individual resources.
- 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:
- size
- number
- size & number
- size & number & totals
- offset
- limit
- offset & limit
- 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:
- The page number
- The page size or limit
- The total number of pages (totalPages) in the collection
- 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:
- Request
- Response
/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:
- sorting a collection by any model attribute.
- sorting a collection by multiple attributes at the same time in either ascending or descending order.
- sorting a collection by an attribute of another model connected via one or more 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:
- Request
- Response
/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 supports a mechanism for extensions.
Elide supports the Atomic Operations Extension which allows multiple mutation operations (create, delete, update) to be bundled together in as single request. Elide also supports the older deprecated JSON Patch Extension which offers similar functionality.
Elide supports these extensions because it allows complex & bulk edits to the data model in the context of a single transaction.
The extensions require a different Media Type to be specified for the Content-Type
and Accept
headers when making
the request.
Extension | Media Type |
---|---|
Atomic Operations | application/vnd.api+json;ext="https://jsonapi.org/ext/atomic" |
JSON Patch | application/vnd.api+json;ext=jsonpatch |
Elide's Atomic Operations and JSON Patch extension support requires that all resources have assigned IDs specified using
the id
member when fixing up relationships. For newly created objects, if the IDs are generated by the server, a
client generated Local ID can be specified using the lid
member. Client generated IDs should be a UUID as described in
RFC 4122.
Atomic Operations
The following Atomic Operations request creates an author (Ernest Hemingway), some of his books, and his book publisher in a single request:
- Request
- Response
{
"atomic:operations":[
{
"op":"add",
"data":{
"lid":"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",
"data":{
"lid":"12345678-1234-1234-1234-1234567890ac",
"type":"book",
"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",
"data":{
"lid":"12345678-1234-1234-1234-1234567890ad",
"type":"book",
"attributes":{
"title":"For Whom the Bell Tolls",
"genre":"Literary Fiction",
"language":"English"
}
}
},
{
"op":"add",
"href":"/book/12345678-1234-1234-1234-1234567890ac/publisher",
"data":{
"lid":"12345678-1234-1234-1234-1234567890ae",
"type":"publisher",
"attributes":{
"name":"Default publisher"
}
}
}
]
}
{
"atomic:results":[
{
"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"
}
}
]
}
JSON Patch
The following JSON Patch request creates an author (Ernest Hemingway), some of his books, and his book publisher in a single request:
- Request
- Response
[
{
"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"
}
}
]
Links
JSON-API links are disabled by default. They can be enabled in application.yaml
:
elide:
base-url: 'https://my-elide.com'
json-api:
enabled: true
path: /json
links:
enabled: true
The elide.json-api.links.enabled
property switches the feature on. The base-url
property provides the URL schema,
host, and port our clients use to connect to our service. The path
property provides the route where the JSON-API
controller is rooted. All link URLs using the above configuration would be prefixed with 'https://my-elide.com/json'.
If base-url
is not provided, Elide will generate the link URL prefix using the client HTTP request.
For Elide standalone, we can enable links by overriding ElideStandaloneSettings
and configure the settings:
public abstract class Settings implements ElideStandaloneSettings {
@Override
public String getBaseUrl() {
return "https://elide.io";
}
@Override
public JsonApiSettingsBuilder getJsonApiSettingsBuilder(EntityDictionary dictionary, JsonApiMapper mapper) {
String jsonApiBaseUrl = getBaseUrl()
+ getJsonApiPathSpec().replace("/*", "")
+ "/";
return ElideStandaloneSettings.super.getJsonApiSettingsBuilder(dictionary, mapper)
.links(links -> links.enabled(true).jsonApiLinks(new DefaultJsonApiLinks(jsonApiBaseUrl)));
}
}
Enabling JSON-API links will result in payload responses that look like:
{
"data": [
{
"type": "group",
"id": "com.example.repository",
"attributes": {
"commonName": "Example Repository",
"description": "The code for this project"
},
"relationships": {
"products": {
"links": {
"self": "https://elide.io/api/v1/group/com.example.repository/relationships/products",
"related": "https://elide.io/api/v1/group/com.example.repository/products"
},
"data": [
]
}
},
"links": {
"self": "https://elide.io/api/v1/group/com.example.repository"
}
}
]
}
We can customize the links that are returned by registering our own implementation of JsonApiLinks
with
ElideSettings
:
public interface JsonApiLinks {
Map<String, String> getResourceLevelLinks(PersistentResource var1);
Map<String, String> getRelationshipLinks(PersistentResource var1, String var2);
}
Meta Blocks
JSON-API supports returning non-standard information in responses inside a meta block. Elide supports meta blocks in three scenarios:
- Document meta blocks are returned for any pagination request.
- The developer can customize the Document meta block for any collection query.
- The developer can customize a Resource meta block for any resource returned by Elide.
Customizing the Document Meta Block
To customize the document meta block, add fields to the RequestScope
object inside a
custom data store:
@Override
public <T> DataStoreIterable<T> loadObjects(EntityProjection projection, RequestScope scope){
//Populates the JSON-API meta block with a new field, 'key':
scope.setMetadataField("key", 123);
}
This would produce a JSON response like:
{
"data": [
{
"type": "widget",
"id": "1"
}
],
"meta": {
"key": 123
}
}
Customizing the Resource Meta Block
To customize the resource meta block, the resource model class must implement the WithMetadata
interface:
public interface WithMetadata {
/**
* Sets a metadata property for this request.
* @param property
* @param value
*/
default void setMetadataField(String property, Object value) { //NOOP }
/**
* Retrieves a metadata property from this request.
* @param property
* @return An optional metadata property.
*/
Optional<Object> getMetadataField(String property);
/**
* Return the set of metadata fields that have been set.
* @return metadata fields that have been set.
*/
Set<String> getMetadataFields();
}
For example, the following example model implements WithMetadata
:
@Include
public class Widget implements WithMetadata {
static Map metadata = Map.of("key", 123);
@Id
private String id;
@Override
public Optional<Object> getMetadataField(String property) {
return Optional.ofNullable(Widget.metadata.get(property));
}
@Override
public Set<String> getMetadataFields() {
return Widget.metadata.keySet();
}
}
The models must be populated with at least one field for the meta block to be returned in the response. These fields can also be populated in a custom data store or lifecycle hook. This would produce a JSON response like:
{
"data": [
{
"type": "widget",
"id": "1",
"meta": {
"key": 123
}
}
]
}
Type Serialization/Deserialization
Type coercion between the API and underlying data model has common support across JSON-API & GraphQL, and is covered here.
OpenAPI
OpenAPI documents can be highly customized. The steps to customize this are documented here.
Custom Error Responses
Configuring custom error responses is documented here.