From 26e1bb391040116cb75c081b90e6c153654d98da Mon Sep 17 00:00:00 2001 From: Arjen Blokzijl Date: Mon, 29 Jun 2026 12:05:05 +0200 Subject: [PATCH] Add JSON:API v1.1 support as an additive overlay (#1) * Add JSON:API v1.1 ruleset as an additive overlay Introduces jsonapi-1.1.yml, which extends the untouched v1.0 ruleset (.spectral.yml) and only redefines the rules that changed in JSON:API v1.1: the ext/profile media type parameters, expanded links object members (rel, describedby, title, type, hreflang), the jsonapi object ext/profile/meta members, and the lid local identifier on resource objects and resource identifier objects. The v1.0 ruleset is unchanged, so existing consumers are unaffected. Spectral does not inherit aliases through extends, so the alias block is duplicated into the overlay (rules are not). Adds an examples/valid-1.1 fixture exercising the new features and a test:1.1 script; npm test now lints both rulesets. Bumps to 1.1.0. * Document JSON:API v1.1 ruleset in README Note both v1.0 (.spectral.yml) and v1.1 (jsonapi-1.1.yml) rulesets, how to select one per API version, and that the v1.1 file is an additive overlay on top of v1.0. --- README.md | 22 +- examples/valid-1.1/valid-example-1.1.yml | 839 +++++++++++++++++++++++ jsonapi-1.1.yml | 296 ++++++++ package.json | 6 +- 4 files changed, 1157 insertions(+), 6 deletions(-) create mode 100644 examples/valid-1.1/valid-example-1.1.yml create mode 100644 jsonapi-1.1.yml diff --git a/README.md b/README.md index 85eab117..914b6c3f 100644 --- a/README.md +++ b/README.md @@ -3,20 +3,34 @@ ![Node.js CI](https://github.com/jmlue42/spectral-jsonapi-ruleset/workflows/Node.js%20CI/badge.svg) ![CodeQL](https://github.com/jmlue42/spectral-jsonapi-ruleset/workflows/CodeQL/badge.svg) -> A [Stoplight/Spectral](https://github.com/stoplightio/spectral) linting ruleset for the [JSON:API specification v1.0](https://jsonapi.org/format/1.0). +> A [Stoplight/Spectral](https://github.com/stoplightio/spectral) linting ruleset for the [JSON:API specification](https://jsonapi.org/), supporting both [v1.0](https://jsonapi.org/format/1.0) and [v1.1](https://jsonapi.org/format/1.1). ## Installation For ways to integrate this ruleset into your Spectral linting suite. See [Sharing & Distributing Rulesets](https://meta.stoplight.io/docs/spectral/docs/guides/7-sharing-rulesets.md) at ## Organization -`.spectral.yml` - Spectral Ruleset +`.spectral.yml` - Spectral Ruleset for JSON:API **v1.0** -The ruleset `extends` - `spectral:oas`, spectral's built-in OAS linting rules. +`jsonapi-1.1.yml` - Spectral Ruleset for JSON:API **v1.1** + +Point Spectral at the file matching the JSON:API version your API targets: + +```bash +# Lint a v1.0 API definition +spectral lint your-api.yml -r ./.spectral.yml + +# Lint a v1.1 API definition +spectral lint your-api.yml -r ./jsonapi-1.1.yml +``` + +The v1.0 ruleset `extends` - `spectral:oas`, spectral's built-in OAS linting rules. + +`jsonapi-1.1.yml` is an additive overlay: it `extends` `.spectral.yml` and only redefines the rules that changed in v1.1 (the `ext`/`profile` media type parameters, the expanded `links` object members, the `jsonapi` object `ext`/`profile`/`meta` members, and the `lid` local identifier). Everything else is inherited unchanged. (Note: Spectral does not inherit `aliases` through `extends`, so the alias block is duplicated in the overlay; keep the two in sync.) The rules are generally organized by the JSON:API specification section the rule is mentioned in. Each rule notes the section url it realates to. -`examples` folder contains valid and invalid OAS3.1 examples +`examples` folder contains valid and invalid OAS3.1 examples. `examples/valid` is linted against the v1.0 ruleset and `examples/valid-1.1` against the v1.1 ruleset. ## Contributing In lieu of a formal style guide (I know... ironic :grin:): diff --git a/examples/valid-1.1/valid-example-1.1.yml b/examples/valid-1.1/valid-example-1.1.yml new file mode 100644 index 00000000..6cd0c586 --- /dev/null +++ b/examples/valid-1.1/valid-example-1.1.yml @@ -0,0 +1,839 @@ +openapi: 3.1.0 +info: + title: Sample JSON:API OAS File + description: Example file on how a JSON:API v1 compliant API can be described in OAS v3.1 + version: 1.0.0 + contact: + name: John Q. Public + email: some_email@domain.com + url: http://www.example.com +tags: + - name: collection + description: tag description + - name: single + description: tag description +servers: + - url: https://api.domain.com/v1 + description: production + - url: https://api-sandbox.domain.com/v1 + description: production sandbox + - url: https://api.{environment}.domain.com/v1 + description: pre-production + variables: + environment: + enum: + - 'dev' + - 'qa' + default: 'dev' +paths: + /myResources: + get: + tags: + - collection + summary: Short Description + description: A longer MEANINGFUL description. Do not copy summary. + operationId: "getResourceList" + security: + - ApiKeyAuth: [] + AuthCode: + - 'actions:read' + parameters: + - $ref: '#/components/parameters/filter' + - $ref: '#/components/parameters/page' + - $ref: '#/components/parameters/sort' + responses: + '200': + $ref: '#/components/responses/MyResource_Collection' + '400': + $ref: '#/components/responses/400Error' + '401': + $ref: '#/components/responses/401Error' + '403': + $ref: '#/components/responses/403Error' + '406': + $ref: '#/components/responses/406Error' + '500': + $ref: '#/components/responses/500Error' + 'default': + $ref: '#/components/responses/DefaultError' + post: + tags: + - single + summary: Create Resource + description: A longer MEANINGFUL description. Do not copy summary. + operationId: "createResource" + security: + - ApiKeyAuth: [] + AuthCode: + - 'actions:write' + requestBody: + $ref: '#/components/requestBodies/MyResource_Post' + responses: + '201': + $ref: '#/components/responses/MyResource_Single' + '400': + $ref: '#/components/responses/400Error' + '401': + $ref: '#/components/responses/401Error' + '403': + $ref: '#/components/responses/403Error' + '406': + $ref: '#/components/responses/406Error' + '409': + $ref: '#/components/responses/409Error' + '415': + $ref: '#/components/responses/415Error' + '500': + $ref: '#/components/responses/500Error' + 'default': + $ref: '#/components/responses/DefaultError' + /myResources/{id}: + get: + tags: + - single + summary: Short Description + description: A longer MEANINGFUL description. Do not copy summary. + operationId: "getResourceById" + security: + - ApiKeyAuth: [] + AuthCode: + - 'actions:read' + parameters: + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/include' + - $ref: '#/components/parameters/fields' + responses: + '200': + $ref: '#/components/responses/MyResource_Single' + '400': + $ref: '#/components/responses/400Error' + '401': + $ref: '#/components/responses/401Error' + '403': + $ref: '#/components/responses/403Error' + '406': + $ref: '#/components/responses/406Error' + '500': + $ref: '#/components/responses/500Error' + 'default': + $ref: '#/components/responses/DefaultError' + patch: + tags: + - single + summary: Short Description + description: A longer MEANINGFUL description. Do not copy summary. + operationId: "updateResourceById" + security: + - ApiKeyAuth: [] + AuthCode: + - 'actions:write' + parameters: + - $ref: '#/components/parameters/id' + requestBody: + $ref: '#/components/requestBodies/MyResource_Patch' + responses: + '200': + $ref: '#/components/responses/MyResource_Single' + '400': + $ref: '#/components/responses/400Error' + '401': + $ref: '#/components/responses/401Error' + '404': + $ref: '#/components/responses/404Error' + '403': + $ref: '#/components/responses/403Error' + '406': + $ref: '#/components/responses/406Error' + '409': + $ref: '#/components/responses/409Error' + '415': + $ref: '#/components/responses/415Error' + '500': + $ref: '#/components/responses/500Error' + 'default': + $ref: '#/components/responses/DefaultError' + delete: + tags: + - single + summary: Short Description + description: A longer MEANINGFUL description. Do not copy summary. + operationId: "deleteResourceById" + security: + - ApiKeyAuth: [] + AuthCode: + - 'actions:delete' + parameters: + - $ref: '#/components/parameters/id' + responses: + '204': + description: Successful Operation. No Content. + '400': + $ref: '#/components/responses/400Error' + '401': + $ref: '#/components/responses/401Error' + '403': + $ref: '#/components/responses/403Error' + '404': + $ref: '#/components/responses/404Error' + '406': + $ref: '#/components/responses/406Error' + '500': + $ref: '#/components/responses/500Error' + 'default': + $ref: '#/components/responses/DefaultError' +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + description: Api Key value. First level of client identification and access control to proxy/gateway + in: header + name: api-key + AuthCode: + type: oauth2 + flows: + authorizationCode: + authorizationUrl: 'https://api.domain.com/oauth/authorize' + tokenUrl: 'https://api.domain.com/oauth/token' + refreshUrl: 'https://api.domain.com/oauth/refresh' + scopes: + 'actions:write': modify actions the end-user has access to + 'actions:read': read actions the end-user has access to + 'actions:delete': delete actions the end-user has access to + headers: + Location: + description: Location of the resource in question + schema: + type: string + example: http://api.domain.com/v1/{resource-colleciton-name}/{id} + WWWAuthenticate: + description: Authentication Challenge Information + schema: + type: string + example: Bearer realm=domain.com + parameters: + id: + name: id + description: resource reference id + in: path + required: true + schema: + type: string + pattern: '^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[afA-F0-9]{4}-[a-fA-F0-9]{12}){1}$' + example: 4257c52f-6c78-4747-8106-e185c081436b + include: + name: include + description: csv formatted parameter of relationship names to include in response + in: query + style: form + explode: false + schema: + type: array + items: + type: string + example: ["relationship","relationship.attribute"] + page: + name: page + description: Paging parameter. + in: query + schema: + type: object + required: ["cursor","limit"] + properties: + cursor: + type: string + limit: + type: integer + format: int32 + style: deepObject + sort: + name: sort + description: csv formatted parameter of fields to sort by + in: query + style: form + explode: false + schema: + type: array + items: + type: string + example: ["-age","name"] + filter: + name: filter + description: schema for 'filter' query parameter + in: query + schema: + type: object + style: deepObject + example: + attribute: "value" + attribute_2: "value1,value2" + relationship.attribute: "value" + fields: + name: fields + description: schema for 'fields' query parameter + in: query + schema: + type: object + style: deepObject + example: + resourceType: "fieldName" + resourceType2: "filedName1,fieldName2" + requestBodies: + MyResource_Patch: + required: true + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/MyResourcePatchObject' + MyResource_Post: + required: true + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/MyResourcePostObject' + responses: + MyResource_Single: + description: Successful Operation + headers: + Location: + $ref: '#/components/headers/Location' + content: + application/vnd.api+json: + schema: + type: object + required: + - links + - data + properties: + links: + type: object + properties: + self: + $ref: '#/components/schemas/Link' + example: https://api.domain.com/v1/myResources/{id} + data: + $ref: '#/components/schemas/MyResourceResponseObject' + MyResource_Collection: + description: Successful Operation + content: + application/vnd.api+json: + schema: + type: object + required: + - links + - data + properties: + jsonapi: + type: object + properties: + version: + type: string + example: '1.1' + ext: + type: array + items: + type: string + format: uri + profile: + type: array + items: + type: string + format: uri + meta: + type: object + links: + type: object + properties: + self: + $ref: '#/components/schemas/Link' + example: https://api.domain.com/v1/myResources + data: + type: array + items: + $ref: '#/components/schemas/MyResourceResponseObject' + 400Error: + description: 'Bad Request' + content: + application/vnd.api+json: + schema: + type: object + required: + - errors + properties: + errors: + type: array + items: + $ref: '#/components/schemas/BaseErrorObject' + description: 'Bad Request' + example: + errors: + - id: e50d3928-c1f0-4e10-888b-b110734656ab + status: "400" + title: Bad Request + source: + parameter: "id" + - id: b6ad7d24-4dec-4ba0-931c-52731a9469e3 + status: "400" + title: Bad Request + source: + pointer: "/data/attributes/name" + 401Error: + description: 'Unauthorized: Invalid or Expired Authentication' + headers: + WWWAuthenticate: + $ref: '#/components/headers/WWWAuthenticate' + content: + application/vnd.api+json: + schema: + type: object + required: + - errors + properties: + errors: + type: array + items: + allOf: + - $ref: '#/components/schemas/BaseErrorObject' + - type: object + description: 'Unauthorized: Invalid or Expired Authentication' + properties: + status: + enum: + - "401" + title: + enum: + - "Unauthorized" + maxItems: 1 + example: + errors: + - id: e50d3928-c1f0-4e10-888b-b110734656ab + status: "401" + title: Unauthorized + 403Error: + description: 'Forbidden: Request does not have necessary permissions' + content: + application/vnd.api+json: + schema: + type: object + required: + - errors + properties: + errors: + type: array + items: + allOf: + - $ref: '#/components/schemas/BaseErrorObject' + - type: object + description: 'Forbidden: Request does not have necessary permissions' + properties: + status: + enum: + - "403" + title: + enum: + - "Forbidden" + maxItems: 1 + example: + errors: + - id: e50d3928-c1f0-4e10-888b-b110734656ab + status: "403" + title: Forbidden + 404Error: + description: 'Not Found' + content: + application/vnd.api+json: + schema: + type: object + required: + - errors + properties: + errors: + type: array + items: + allOf: + - $ref: '#/components/schemas/BaseErrorObject' + - type: object + description: 'Not Found' + properties: + status: + enum: + - "404" + title: + enum: + - "Not Found" + maxItems: 1 + example: + errors: + - id: e50d3928-c1f0-4e10-888b-b110734656ab + status: "404" + title: Not Found + 406Error: + description: 'Not Acceptable' + content: + application/vnd.api+json: + schema: + type: object + required: + - errors + properties: + errors: + type: array + items: + allOf: + - $ref: '#/components/schemas/BaseErrorObject' + - type: object + description: 'Not Acceptable' + properties: + status: + enum: + - "406" + title: + enum: + - "Not Acceptable" + maxItems: 1 + example: + errors: + - id: e50d3928-c1f0-4e10-888b-b110734656ab + status: "406" + title: Not Acceptable + 409Error: + description: 'Conflict' + content: + application/vnd.api+json: + schema: + type: object + required: + - errors + properties: + errors: + type: array + items: + allOf: + - $ref: '#/components/schemas/BaseErrorObject' + - type: object + description: 'Conflict' + properties: + status: + enum: + - "409" + title: + enum: + - "Conflict" + maxItems: 1 + example: + errors: + - id: e50d3928-c1f0-4e10-888b-b110734656ab + status: "409" + title: Conflict + 415Error: + description: 'Unsupported Media Type' + content: + application/vnd.api+json: + schema: + type: object + required: + - errors + properties: + errors: + type: array + items: + allOf: + - $ref: '#/components/schemas/BaseErrorObject' + - type: object + description: 'Unsupported Media Type' + properties: + status: + enum: + - "415" + title: + enum: + - "Unsupported Media Type" + maxItems: 1 + example: + errors: + - id: e50d3928-c1f0-4e10-888b-b110734656ab + status: "415" + title: Unsupported Media Type + 500Error: + description: 'Internal Server Error' + content: + application/vnd.api+json: + schema: + type: object + required: + - errors + properties: + errors: + type: array + items: + $ref: '#/components/schemas/BaseErrorObject' + description: 'Internal Server Error' + example: + errors: + - id: e50d3928-c1f0-4e10-888b-b110734656ab + status: "400" + title: Bad Request + source: + parameter: "id" + - id: b6ad7d24-4dec-4ba0-931c-52731a9469e3 + status: "500" + title: Internal Server Error + DefaultError: + description: Error + content: + application/vnd.api+json: + schema: + type: object + required: + - errors + properties: + errors: + type: array + items: + $ref: '#/components/schemas/BaseErrorObject' + maxItems: 1 + schemas: + MyResourceResponseObject: + allOf: + - $ref: '#/components/schemas/IdentifierObject' + - type: object + required: + - attributes + - relationships + properties: + type: + enum: + - resources + attributes: + type: object + required: + - name + properties: + account_id: + type: string + name: + type: string + example: do-hickey + description: + type: string + example: thing that does stuff + relationships: + type: object + properties: + manufacturer: + type: object + required: + - links + - data + properties: + links: + type: object + required: + - self + - related + properties: + self: + $ref: '#/components/schemas/Link' + example: http://api.domain.com/v1/myResources/{id}/relationships/manufacturers + related: + type: string + example: http://api.domain.com/v1/manufacturers/{id} + data: + allOf: + - $ref: '#/components/schemas/RelationshipDataObject' + - type: object + properties: + type: + enum: + - manufacturers + MyResourcePostObject: + type: object + required: + - type + - attributes + properties: + type: + type: string + enum: + - resources + attributes: + type: object + required: + - name + properties: + name: + type: string + example: do-hickey + description: + type: string + example: thing that does stuff + relationships: + type: object + required: + - manufacturer + properties: + manufacturer: + type: object + required: + - data + properties: + data: + type: object + required: + - id + - type + properties: + id: + type: string + pattern: '^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[afA-F0-9]{4}-[a-fA-F0-9]{12}){1}$' + example: 2357c52f-6c78-4747-8106-e185c08143aa + type: + type: string + enum: + - manufacturers + MyResourcePatchObject: + allOf: + - $ref: '#/components/schemas/IdentifierObject' + - type: object + required: + - attributes + properties: + type: + enum: + - resources + attributes: + type: object + required: + - name + properties: + name: + type: string + example: do-hickey + description: + type: + - 'null' + - string + example: thing that does stuff + relationships: + type: object + required: + - manufacturer + properties: + manufacturer: + type: object + required: + - data + properties: + data: + allOf: + - $ref: '#/components/schemas/RelationshipDataObject' + - type: object + properties: + type: + enum: + - manufacturers + Link: + oneOf: + - type: string + - type: object + required: + - href + properties: + href: + type: string + rel: + type: string + describedby: + $ref: '#/components/schemas/Link' + title: + type: string + type: + type: string + hreflang: + type: string + meta: + type: object + IdentifierObject: + type: object + required: + - id + - type + properties: + id: + type: string + pattern: '^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[afA-F0-9]{4}-[a-fA-F0-9]{12}){1}$' + example: 4257c52f-6c78-4747-8106-e185c081436b + lid: + type: string + description: A v1.1 local identifier, unique within the document. + type: + type: string + meta: + type: object + RelationshipDataObject: + type: object + required: + - id + - type + properties: + id: + type: string + pattern: '^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[afA-F0-9]{4}-[a-fA-F0-9]{12}){1}$' + example: 2357c52f-6c78-4747-8106-e185c08143aa + lid: + type: string + description: A v1.1 local identifier for linkage to a client-generated resource. + type: + type: string + BaseErrorObject: + type: object + description: JSON:API Error Object + properties: + id: + type: string + description: a unique identifier for this particular occurrence of the problem + links: + type: object + description: links that lead to further detail about the particular occurrence of the problem + properties: + about: + $ref: '#/components/schemas/Link' + status: + type: string + description: the HTTP status code applicable to this problem + code: + type: string + description: an application-specific error code + title: + type: string + description: a human-readable summary specific of the problem. Usually the http status friendly name. + detail: + type: string + description: a human-readable explanation specific to this occurrence of the problem + source: + type: object + description: an object containing references to the source of the error + properties: + pointer: + description: a JSON Pointer [RFC6901] to the associated entity in the request document + oneOf: + - type: string + format: json-pointer + - type: array + items: + type: string + format: json-pointer + parameter: + description: a string indicating which URI query parameter caused the error + type: string + meta: + type: object diff --git a/jsonapi-1.1.yml b/jsonapi-1.1.yml new file mode 100644 index 00000000..ee0e0c91 --- /dev/null +++ b/jsonapi-1.1.yml @@ -0,0 +1,296 @@ +description: "# [{json:api}](https://jsonapi.org/) - [v1.1](https://jsonapi.org/format/1.1/)\r\n> + A Specification for Building APIs in JSON\r\n\r\nThis ruleset is an **additive overlay** on top of + the v1.0 ruleset (`.spectral.yml`). It inherits every v1.0 rule and its aliases unchanged, and + only redefines the handful of rules whose behaviour was relaxed or extended in + [JSON:API v1.1](https://jsonapi.org/format/1.1/): the `ext`/`profile` media type parameters, + the expanded `links` object members, the new `jsonapi` object members (`ext`, `profile`, `meta`), + and the `lid` local-identifier member on resource objects and resource identifier objects.\r\n\r\nPoint + Spectral at this file to lint a JSON:API **v1.1** definition; keep using `.spectral.yml` for v1.0." + +# Inherit the complete v1.0 ruleset (rules + aliases + spectral:oas). Redefining a rule +# below by its key REPLACES the inherited definition; every other rule is inherited as-is. +extends: + - ./.spectral.yml + +formats: + - oas3.1 + +# NOTE: Spectral does not inherit `aliases` through `extends`, so the alias block from +# `.spectral.yml` is duplicated here verbatim. Keep the two in sync if aliases change. +aliases: + AllContentSchemas: + - "$.paths..content['application/vnd.api+json'].schema" + + ResourceObjects: + - "$.paths..responses..content[application/vnd.api+json].schema.properties.data.properties" + - "$.paths..responses..content[application/vnd.api+json].schema.properties.data.allOf[*].properties" + - "$.paths..responses..content[application/vnd.api+json].schema.properties.data.items.properties" + - "$.paths..responses..content[application/vnd.api+json].schema.properties.data.items.allOf[*].properties" + - "$.paths..content[application/vnd.api+json].schema.properties.included.items.properties" + - "$.paths..content[application/vnd.api+json].schema.properties.included.items.allOf[*].properties" + - "$.paths..patch.requestBody.content[application/vnd.api+json].schema.properties.data.properties" + - "$.paths..patch.requestBody.content[application/vnd.api+json].schema.properties.data.allOf[*].properties" + + POSTResourceObjects: + - "$.paths..post.requestBody.content[application/vnd.api+json].schema.properties.data.properties" + - "$.paths..post.requestBody.content[application/vnd.api+json].schema.properties.data.allOf[*].properties" + + LinkObjects: + - "#AllContentSchemas..properties[links]" + + MetaObjects: + - "#AllContentSchemas..properties[meta]" + + Relationships: + - "#AllContentSchemas..properties[relationships]" + + RelationshipData: + - "#Relationships..data" + + POSTRelationships: + - "$.paths..post.requestBody.content[application/vnd.api+json].schema.properties.data.properties[relationships].properties[*]" + - "$.paths..post.requestBody.content[application/vnd.api+json].schema.properties.data.allOf[*].properties[relationships].properties[*]" + + PATCHRelationships: + - "$.paths..patch.requestBody.content[application/vnd.api+json].schema.properties.data.properties[relationships].properties[*]" + - "$.paths..patch.requestBody.content[application/vnd.api+json].schema.properties.data.allOf[*].properties[relationships].properties[*]" + + SingleErrorResponses: + - "$.paths..responses[?(@property > '400' && @property < '500')].content[application/vnd.api+json].schema.properties.errors" + - "$.paths..responses[?(@property > '500' && @property < '600')].content[application/vnd.api+json].schema.properties.errors" + - "$.paths..responses[default].content[application/vnd.api+json].schema.properties.errors" + + ErrorObjects: + - "$.paths..responses[default,400,500].content[application/vnd.api+json].schema.properties.errors.items.properties" + - "$.paths..responses[default,400,500].content[application/vnd.api+json].schema.properties.errors.items.allOf[*].properties" + - "$.paths..responses[?(@property > '400' && @property < '500')].content[application/vnd.api+json].schema.properties.errors.items.properties" + - "$.paths..responses[?(@property > '400' && @property < '500')].content[application/vnd.api+json].schema.properties.errors.items.allOf[*].properties" + - "$.paths..responses[?(@property > '500' && @property < '600')].content[application/vnd.api+json].schema.properties.errors.items.properties" + - "$.paths..responses[?(@property > '500' && @property < '600')].content[application/vnd.api+json].schema.properties.errors.items.allOf[*].properties" + +rules: + +# --------------------------------------------------------------------------- +# Section 4 Content Negotiation (v1.1) +# v1.1 permits the `ext` and `profile` media type parameters on the JSON:API media type. +# https://jsonapi.org/format/1.1/#content-negotiation +# --------------------------------------------------------------------------- + + content-type: + description: "Clients and Servers **MUST** send all JSON:API data as Content-Type: + `application/vnd.api+json`. In v1.1 this media type **MAY** carry the `ext` and/or + `profile` media type parameters (and no others).\r\n\r\n**Valid Examples:**\r\n```YAML\r\napplication/vnd.api+json\r\napplication/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic\"\r\napplication/vnd.api+json; profile=\"https://example.com/resource-timestamps\"\r\n```\r\n\r\nRelated + specification information can be found [here](https://jsonapi.org/format/1.1/#content-negotiation-servers)." + documentationUrl: "https://jsonapi.org/format/1.1/#content-negotiation" + message: "content MUST be 'application/vnd.api+json', optionally with only 'ext'/'profile' media type parameters" + severity: error + given: + - "$.paths..requestBody.content" + - "$.paths..responses..content" + then: + field: "@key" + function: pattern + functionOptions: + match: '^application/vnd\.api\+json(\s*;\s*(ext|profile)="[^"]*")*$' + +# --------------------------------------------------------------------------- +# Section 5.4 Links (v1.1) +# v1.1 link objects gain `rel`, `describedby`, `title`, `type`, `hreflang` members. +# https://jsonapi.org/format/1.1/#document-links +# --------------------------------------------------------------------------- + + links-object-schema-properties: + description: "Objects contained within a `links` object **MUST** contain `href` + (string) and **MAY** contain `rel`, `describedby`, `title`, `type`, `hreflang` + and `meta` (v1.1).\r\n\r\nRelated specification information can be found [here](https://jsonapi.org/format/1.1/#document-links)." + documentationUrl: "https://jsonapi.org/format/1.1/#document-links" + message: "link objects MUST contain 'href' (string) and MAY contain 'rel', 'describedby', 'title', 'type', 'hreflang', 'meta'" + severity: error + given: "#LinkObjects.properties..properties" + then: + - field: "@key" + function: enumeration + functionOptions: + values: + - href + - rel + - describedby + - title + - type + - hreflang + - meta + - field: href + function: truthy + - field: href.type + function: enumeration + functionOptions: + values: + - string + +# --------------------------------------------------------------------------- +# Section 5.7 JSON:API Object (v1.1) +# v1.1 adds `ext`, `profile` and `meta` members; `version` remains optional. +# https://jsonapi.org/format/1.1/#document-jsonapi-object +# --------------------------------------------------------------------------- + + jsonapi-object: + description: "`jsonapi` object **MUST** match schema (v1.1).\r\n\r\n**Schema Rules:**\r\n- + `jsonapi` **MUST** be an `object`\r\n- **MAY** contain `version` (`string`), `ext` + (`array`), `profile` (`array`) and `meta` (`object`)\r\n\r\n**Valid Example:**\r\n```YAML\r\nproperties:\r\n + \ jsonapi:\r\n type: object\r\n properties:\r\n version:\r\n type: string\r\n example: '1.1'\r\n ext:\r\n type: array\r\n profile:\r\n type: array\r\n meta:\r\n type: object\r\n```\r\n\r\nRelated + specification information can be found [here](https://jsonapi.org/format/1.1/#document-jsonapi-object)." + documentationUrl: "https://jsonapi.org/format/1.1/#document-jsonapi-object" + message: "jsonapi object MUST match schema" + severity: error + given: "#AllContentSchemas..properties[?(@property === 'jsonapi')]" + then: + - field: type + function: enumeration + functionOptions: + values: + - object + - field: "properties[*]~" + function: enumeration + functionOptions: + values: + - version + - ext + - profile + - meta + - field: properties.version.type + function: enumeration + functionOptions: + values: + - string + +# --------------------------------------------------------------------------- +# Section 5.2 Resource Objects - Identification (v1.1) +# v1.1 adds the `lid` (local id) member, valid wherever `id` is. +# https://jsonapi.org/format/1.1/#document-resource-object-identification +# --------------------------------------------------------------------------- + + resource-object-properties: + description: "Verify allowed properties in Resource Objects (v1.1).\r\n\r\n**Allowed + properties:** `id`, `lid`, `type`, `attributes`, `relationships`, `links`, `meta`\r\n\r\nRelated + specification information can be found [here](https://jsonapi.org/format/1.1/#document-resource-objects)." + documentationUrl: "https://jsonapi.org/format/1.1/#document-resource-objects" + message: "'data' objects/items MUST meet Resource Object restrictions" + severity: error + given: + - "#ResourceObjects" + - "#POSTResourceObjects" + then: + - field: type + function: truthy + - field: "@key" + function: enumeration + functionOptions: + values: + - id + - lid + - type + - attributes + - relationships + - links + - meta + + resource-object-id-required: + description: "A Resource Object **MUST** contain an `id` (or, for client-generated + resources in v1.1, a `lid`) - except in a POST requestBody where neither is required.\r\n\r\n**NOTE:** + Currently this rule triggers against `allOf` structures unless all items have `id`/`lid`. + Until this is corrected it is set as a warning.\r\n\r\nRelated specification information + can be found [here](https://jsonapi.org/format/1.1/#document-resource-object-identification)." + documentationUrl: "https://jsonapi.org/format/1.1/#document-resource-object-identification" + message: "Could be missing 'id'/'lid' property. Please verify the resource." + severity: warn + given: "#ResourceObjects" + then: + function: schema + functionOptions: + dialect: "draft2020-12" + schema: + type: object + anyOf: + - required: ["id"] + - required: ["lid"] + +# --------------------------------------------------------------------------- +# Section 8 Resource Identifier Objects (v1.1) +# v1.1 allows `lid` in resource identifier objects (relationship linkage). +# https://jsonapi.org/format/1.1/#document-resource-identifier-objects +# --------------------------------------------------------------------------- + + relationship-data-properties: + description: "Relationship `data` (resource identifier object) **MAY** only contain: + `id`, `lid`, `type` and `meta` (v1.1).\r\n\r\nRelated specification information can + be found [here](https://jsonapi.org/format/1.1/#document-resource-identifier-objects)." + documentationUrl: "https://jsonapi.org/format/1.1/#document-resource-identifier-objects" + message: "relationship data May only contain: 'id', 'lid', 'type' and 'meta'" + severity: error + given: + - "#RelationshipData.properties" + - "#RelationshipData.allOf[*].properties" + - "#RelationshipData.items.properties" + - "#RelationshipData.items.allOf[*].properties" + then: + field: "@key" + function: enumeration + functionOptions: + values: + - id + - lid + - type + - meta + + relationship-data-schema: + description: "Relationship data items **MUST** follow schema (v1.1).\r\n\r\n**Schema + Rules:**\r\n- **MUST** contain `type` (`string`) and at least one of `id`/`lid` (`string`)\r\n- + `meta` **MUST** be an `object`\r\n\r\nRelated specification information can be found + [here](https://jsonapi.org/format/1.1/#document-resource-identifier-objects)." + documentationUrl: "https://jsonapi.org/format/1.1/#document-resource-identifier-objects" + message: "relationship data items MUST follow schema" + severity: error + given: + - "#RelationshipData.properties" + - "#RelationshipData.allOf[0].properties" + - "#RelationshipData.items.properties" + - "#RelationshipData.items.allOf[0].properties" + then: + function: schema + functionOptions: + dialect: "draft2020-12" + schema: + type: object + required: ["type"] + anyOf: + - required: ["id"] + - required: ["lid"] + properties: + id: + type: object + properties: + type: + type: string + enum: + - string + lid: + type: object + properties: + type: + type: string + enum: + - string + type: + type: object + properties: + type: + type: string + enum: + - string + meta: + type: object + properties: + type: + type: string + enum: + - object diff --git a/package.json b/package.json index f0d9f8c4..e1caa381 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "name": "spectral-jsonapi-ruleset", - "version": "1.0.0", + "version": "1.1.0", "description": "spectral rules for json:api", "scripts": { - "test": "./node_modules/.bin/spectral lint examples/valid/* -r ./.spectral.yml", + "test": "npm run test:1.0 && npm run test:1.1", + "test:1.0": "./node_modules/.bin/spectral lint examples/valid/* -r ./.spectral.yml", + "test:1.1": "./node_modules/.bin/spectral lint examples/valid-1.1/* -r ./jsonapi-1.1.yml", "prepare": "husky install" }, "repository": {