diff --git a/js/config.mjs b/js/config.mjs index 2135ada..87db506 100644 --- a/js/config.mjs +++ b/js/config.mjs @@ -94,12 +94,12 @@ loadRespecWithConfiguration({ ], github: "https://github.com/Logius-standaarden/API-Design-Rules", pubDomain: "api", - publishDate: "2025-08-27", - publishVersion: "2.1.0", - previousPublishDate: "2025-02-17", - previousPublishVersion: "2.0.2", + publishDate: "2026-01-12", + publishVersion: "2.2.0", + previousPublishDate: "2025-08-27", + previousPublishVersion: "2.1.0", shortName: "adr", - specStatus: "WV", + specStatus: "CV", specType: "ST", pluralize: true, diff --git a/linter/spectral.yml b/linter/spectral.yml index da7089a..d1a7d2b 100644 --- a/linter/spectral.yml +++ b/linter/spectral.yml @@ -183,8 +183,9 @@ rules: functionOptions: match: ^https://.* + #/core/error-handling/problem-details nlgov:use-problem-schema: - severity: warn + severity: error message: "The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457." given: $..[responses][?(@property && @property.match(/(4|5)\d\d/))].content then: @@ -195,6 +196,77 @@ rules: - required: ["application/problem+json"] - required: ["application/problem+xml"] + nlgov:problem-schema-members: + severity: error + message: "{{error}}. These fields are required: status, title and detail." + given: $..[responses][?(@property && @property.match(/(4|5)\d\d/))].content[?(@property=="application/problem+json" || @property=="application/problem+xml")]..schema + then: + function: schema + functionOptions: + schema: + type: object + properties: + properties: + type: object + required: + - status + - title + - detail + + #/core/error-handling/invalid-input + nlgov:problem-invalid-input: + severity: error + message: "GET endpoints that have parameters and all other endpoints must be able to return a 400 response" + given: + - $.paths..[?( @property.match(/get/) && @.parameters && @.parameters.length > 0 )] + - $.paths..[?( @property.match(/(put)|(post)|(delete)|(patch)/))] + then: + function: schema + functionOptions: + schema: + type: object + properties: + responses: + type: object + required: + - "400" + + #/core/error-handling/bad-request + nlgov:problem-schema-members-bad-request: + severity: error + given: $..[responses][?(@property && @property.match(400))].content[?(@property=="application/problem+json" || @property=="application/problem+xml")]..schema.properties + then: + function: schema + functionOptions: + schema: + type: object + properties: + errors: + type: object + properties: + properties: + type: object + properties: + in: {} + detail: {} + location: + type: object + properties: + properties: + type: object + properties: + pointer: {} + name: {} + index: {} + additionalProperties: false + code: {} + required: + - in + - detail + additionalProperties: false + required: + - errors + nlgov:property-casing: severity: warn given: @@ -206,6 +278,56 @@ rules: field: "@key" message: Properties must be lowerCamelCase. + #/core/date-time/timezone + nlgov:date-time-ensure-timezone: + severity: error + given: $..properties[*].format + message: "Use date-time format which includes a timezone" + then: + function: pattern + functionOptions: + notMatch: "/^date-time-local$/" + + nlgov:time-without-timezone: + severity: error + given: $..properties[*].format + message: "Use time-local format without a timezone" + then: + function: pattern + functionOptions: + notMatch: "/^time$/" + + #/core/date-time/date-omit-time-portion + nlgov:specify-format-for-date-and-time: + severity: error + given: + - $..properties[date,datum] + - $..properties[?(@property && @property.match(/((\w+D)|(_[dD]))((ate)|(atum))/))] + message: "Any date field must set 'format' to 'date'" + then: + function: schema + functionOptions: + schema: + anyOf: + - required: ["format"] + - properties: + allOf: + type: array + items: + required: ["format"] + required: ["allOf"] + + nlgov:use-date-instead-of-datetime: + severity: error + given: + - $..properties[date,datum]..format + - $..properties[?(@property && @property.match(/((\w+D)|(_[dD]))((ate)|(atum))/))]..format + message: "Field represents a date and therefore must set 'format' to 'date'" + then: + function: pattern + functionOptions: + notMatch: "/^date-time$/" + nlgov:semver: severity: error message: "Version {{value}} is not in semver format." diff --git a/linter/testcases/cor-api/expected-output.txt b/linter/testcases/cor-api/expected-output.txt index e88c108..9840868 100644 --- a/linter/testcases/cor-api/expected-output.txt +++ b/linter/testcases/cor-api/expected-output.txt @@ -1,28 +1,29 @@ /testcases/cor-api/openapi.json - 70:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./heartbeat.get.responses[429].content - 80:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./heartbeat.get.responses[503].content - 181:29 error nlgov:paths-kebab-case /laatsteWijziging is not kebab-case. paths./laatsteWijziging - 211:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[400].content - 221:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[404].content - 231:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[405].content - 241:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[406].content - 251:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[429].content - 261:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[500].content - 271:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[503].content - 506:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[400].content - 516:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[404].content - 526:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[405].content - 536:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[406].content - 546:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[429].content - 556:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[500].content - 566:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[503].content - 684:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[400].content - 694:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[404].content - 704:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[405].content - 714:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[406].content - 724:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[429].content - 734:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[500].content - 744:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[503].content + 70:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./heartbeat.get.responses[429].content + 80:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./heartbeat.get.responses[503].content + 181:29 error nlgov:paths-kebab-case /laatsteWijziging is not kebab-case. paths./laatsteWijziging + 211:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[400].content + 221:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[404].content + 231:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[405].content + 241:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[406].content + 251:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[429].content + 261:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[500].content + 271:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./laatsteWijziging.get.responses[503].content + 506:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[400].content + 516:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[404].content + 526:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[405].content + 536:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[406].content + 546:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[429].content + 556:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[500].content + 566:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties.get.responses[503].content + 684:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[400].content + 694:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[404].content + 704:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[405].content + 714:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[406].content + 724:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[429].content + 734:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[500].content + 744:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./organisaties/{oin}.get.responses[503].content + 978:27 error nlgov:use-date-instead-of-datetime Field represents a date and therefore must set 'format' to 'date' components.schemas.LocalDateTime.format -✖ 24 problems (1 error, 23 warnings, 0 infos, 0 hints) +✖ 25 problems (25 errors, 0 warnings, 0 infos, 0 hints) diff --git a/linter/testcases/date-time/expected-output.txt b/linter/testcases/date-time/expected-output.txt new file mode 100644 index 0000000..6a3cdc8 --- /dev/null +++ b/linter/testcases/date-time/expected-output.txt @@ -0,0 +1,14 @@ + +/testcases/date-time/openapi.json + 94:55 error nlgov:date-time-ensure-timezone Use date-time format which includes a timezone paths./resources-with-time-incorrect.get.responses[200].content.application/json.schema.properties.date-time-local.format + 98:55 error nlgov:use-date-instead-of-datetime Field represents a date and therefore must set 'format' to 'date' paths./resources-with-time-incorrect.get.responses[200].content.application/json.schema.properties.date.format + 102:55 error nlgov:use-date-instead-of-datetime Field represents a date and therefore must set 'format' to 'date' paths./resources-with-time-incorrect.get.responses[200].content.application/json.schema.properties.datum.format + 106:55 error nlgov:use-date-instead-of-datetime Field represents a date and therefore must set 'format' to 'date' paths./resources-with-time-incorrect.get.responses[200].content.application/json.schema.properties.geboorteDatum.format + 110:55 error nlgov:use-date-instead-of-datetime Field represents a date and therefore must set 'format' to 'date' paths./resources-with-time-incorrect.get.responses[200].content.application/json.schema.properties.birthDate.format + 114:55 error nlgov:use-date-instead-of-datetime Field represents a date and therefore must set 'format' to 'date' paths./resources-with-time-incorrect.get.responses[200].content.application/json.schema.properties.expiration_date.format + 118:55 error nlgov:use-date-instead-of-datetime Field represents a date and therefore must set 'format' to 'date' paths./resources-with-time-incorrect.get.responses[200].content.application/json.schema.properties.expiration_Date.format + 122:55 error nlgov:time-without-timezone Use time-local format without a timezone paths./resources-with-time-incorrect.get.responses[200].content.application/json.schema.properties.timestamp.format + 124:61 error nlgov:specify-format-for-date-and-time Any date field must set 'format' to 'date' paths./resources-with-time-incorrect.get.responses[200].content.application/json.schema.properties.missingFormatDate + 218:27 error nlgov:use-date-instead-of-datetime Field represents a date and therefore must set 'format' to 'date' components.schemas.LocalDateTimeIncorrect.format + +✖ 10 problems (10 errors, 0 warnings, 0 infos, 0 hints) diff --git a/linter/testcases/date-time/openapi.json b/linter/testcases/date-time/openapi.json new file mode 100644 index 0000000..6a4349b --- /dev/null +++ b/linter/testcases/date-time/openapi.json @@ -0,0 +1,238 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Baseline", + "description": "Deze OpenAPI specification bevat het minimale om aan alle regels te voldoen.", + "contact": { + "name": "Beheerder", + "url": "https://www.example.com", + "email": "mail@example.com" + }, + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://example.com/api/v1" + } + ], + "security": [ + { + "default": [] + } + ], + "tags": [ + { + "name": "openapi" + }, + { + "name": "time" + } + ], + "paths": { + "/openapi.json": { + "get": { + "tags": [ + "openapi" + ], + "description": "OpenAPI document", + "operationId": "getOpenapiJSON", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + }, + "access-control-allow-origin": { + "description": "Alle origins mogen bij deze resource", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + }, + "/resources-with-time-incorrect": { + "get": { + "tags": [ + "time" + ], + "description": "Resources with time incorrect", + "operationId": "getResourcesIncorrect", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "date-time-local": { + "type": "string", + "format": "date-time-local" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "datum": { + "type": "string", + "format": "date-time" + }, + "geboorteDatum": { + "type": "string", + "format": "date-time" + }, + "birthDate": { + "type": "string", + "format": "date-time" + }, + "expiration_date": { + "type": "string", + "format": "date-time" + }, + "expiration_Date": { + "type": "string", + "format": "date-time" + }, + "timestamp": { + "type": "string", + "format": "time" + }, + "missingFormatDate": { + "nullable": true + }, + "meerdereDatum": { + "type": "string", + "allOf": [ + { + "$ref": "#/components/schemas/LocalDateTimeIncorrect" + } + ] + } + } + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + }, + "/resources-with-time-correct": { + "get": { + "tags": [ + "time" + ], + "description": "Resources with time correct", + "operationId": "getResourcesCorrect", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "datum": { + "type": "string", + "format": "date" + }, + "geboorteDatum": { + "type": "string", + "format": "date" + }, + "birthDate": { + "type": "string", + "format": "date" + }, + "timestamp": { + "type": "string", + "format": "time-local" + }, + "meerdereDatum": { + "type": "string", + "allOf": [ + { + "$ref": "#/components/schemas/LocalDateTimeCorrect" + } + ] + } + } + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + } + }, + "components": { + "schemas": { + "LocalDateTimeIncorrect": { + "format": "date-time", + "type": "string" + }, + "LocalDateTimeCorrect": { + "format": "date", + "type": "string" + } + }, + "securitySchemes": { + "default": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://test.com", + "scopes": {} + } + } + } + } + } +} \ No newline at end of file diff --git a/linter/testcases/error-type-bad-invalid-input/expected-output.txt b/linter/testcases/error-type-bad-invalid-input/expected-output.txt new file mode 100644 index 0000000..249d395 --- /dev/null +++ b/linter/testcases/error-type-bad-invalid-input/expected-output.txt @@ -0,0 +1,7 @@ + +/testcases/error-type-bad-invalid-input/openapi.json + 119:29 error nlgov:problem-invalid-input GET endpoints that have parameters and all other endpoints must be able to return a 400 response paths./invalid-response-vereist.get.responses + 157:29 error nlgov:problem-invalid-input GET endpoints that have parameters and all other endpoints must be able to return a 400 response paths./invalid-response-vereist.put.responses + 195:29 error nlgov:problem-invalid-input GET endpoints that have parameters and all other endpoints must be able to return a 400 response paths./invalid-response-vereist.post.responses + +✖ 3 problems (3 errors, 0 warnings, 0 infos, 0 hints) diff --git a/linter/testcases/error-type-bad-invalid-input/openapi.json b/linter/testcases/error-type-bad-invalid-input/openapi.json new file mode 100644 index 0000000..c8ab926 --- /dev/null +++ b/linter/testcases/error-type-bad-invalid-input/openapi.json @@ -0,0 +1,232 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Baseline", + "description": "Deze OpenAPI specification bevat het minimale om aan alle regels te voldoen.", + "contact": { + "name": "Beheerder", + "url": "https://www.example.com", + "email": "mail@example.com" + }, + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://example.com/api/v1" + } + ], + "security": [ + { + "default": [] + } + ], + "tags": [ + { + "name": "openapi" + }, + { + "name": "resource" + } + ], + "paths": { + "/openapi.json": { + "get": { + "tags": [ + "openapi" + ], + "description": "OpenAPI document", + "operationId": "getOpenapiJSON", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + }, + "access-control-allow-origin": { + "description": "Alle origins mogen bij deze resource", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "OK", + "content": { + "application/problem+json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "integer" }, + "title": { "type": "string" }, + "detail": { "type": "string" }, + "errors": { + "type": "object", + "properties": { + "in": { "type": "string" }, + "location": { + "type": "object", + "properties": { + "pointer": { "type": "string" }, + "name": { "type": "string" }, + "index": { "type": "integer" } + } + }, + "code": { "type": "string" }, + "detail": { "type": "string" } + } + } + }, + "required": ["status", "title", "detail", "errors"], + "additionalProperties": false + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + }, + "/invalid-response-vereist": { + "get": { + "tags": [ + "resource" + ], + "description": "Resource", + "operationId": "getPropertiesCorrect", + "parameters": [ + { + "name": "expand", + "in": "query", + "description": "Schakelaar om details van gekoppelde organisaties (subOIN of OINhouder) op te vragen (default false = geen details)", + "schema": { + "type": "boolean" + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + }, + "put": { + "tags": [ + "resource" + ], + "description": "Resource", + "operationId": "putPropertiesCorrect", + "parameters": [ + { + "name": "expand", + "in": "query", + "description": "Schakelaar om details van gekoppelde organisaties (subOIN of OINhouder) op te vragen (default false = geen details)", + "schema": { + "type": "boolean" + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + }, + "post": { + "tags": [ + "resource" + ], + "description": "Resource", + "operationId": "postPropertiesCorrect", + "parameters": [ + { + "name": "expand", + "in": "query", + "description": "Schakelaar om details van gekoppelde organisaties (subOIN of OINhouder) op te vragen (default false = geen details)", + "schema": { + "type": "boolean" + }, + "style": "form", + "explode": true + } + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + } + }, + "components": { + "schemas": { + }, + "securitySchemes": { + "default": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://test.com", + "scopes": {} + } + } + } + } + } +} \ No newline at end of file diff --git a/linter/testcases/error-type-bad-request/expected-output.txt b/linter/testcases/error-type-bad-request/expected-output.txt new file mode 100644 index 0000000..7dbc0bd --- /dev/null +++ b/linter/testcases/error-type-bad-request/expected-output.txt @@ -0,0 +1,6 @@ + +/testcases/error-type-bad-request/openapi.json + 133:58 error nlgov:problem-schema-members-bad-request Property "extra" is not expected to be here paths./properties-correct.get.responses[400].content.application/problem+json.schema.properties.errors.properties + 254:66 error nlgov:problem-schema-members-bad-request Property "pointer2" is not expected to be here paths./properties-location-verkeerd-format.get.responses[400].content.application/problem+json.schema.properties.errors.properties.location.properties + +✖ 2 problems (2 errors, 0 warnings, 0 infos, 0 hints) diff --git a/linter/testcases/error-type-bad-request/openapi.json b/linter/testcases/error-type-bad-request/openapi.json new file mode 100644 index 0000000..1ee4a30 --- /dev/null +++ b/linter/testcases/error-type-bad-request/openapi.json @@ -0,0 +1,295 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Baseline", + "description": "Deze OpenAPI specification bevat het minimale om aan alle regels te voldoen.", + "contact": { + "name": "Beheerder", + "url": "https://www.example.com", + "email": "mail@example.com" + }, + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://example.com/api/v1" + } + ], + "security": [ + { + "default": [] + } + ], + "tags": [ + { + "name": "openapi" + }, + { + "name": "resource" + } + ], + "paths": { + "/openapi.json": { + "get": { + "tags": [ + "openapi" + ], + "description": "OpenAPI document", + "operationId": "getOpenapiJSON", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + }, + "access-control-allow-origin": { + "description": "Alle origins mogen bij deze resource", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "OK", + "content": { + "application/problem+json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "integer" }, + "title": { "type": "string" }, + "detail": { "type": "string" }, + "errors": { + "type": "object", + "properties": { + "in": { "type": "string" }, + "location": { + "type": "object", + "properties": { + "pointer": { "type": "string" }, + "name": { "type": "string" }, + "index": { "type": "integer" } + } + }, + "code": { "type": "string" }, + "detail": { "type": "string" } + } + } + }, + "required": ["status", "title", "detail", "errors"], + "additionalProperties": false + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + }, + "/properties-correct": { + "get": { + "tags": [ + "resource" + ], + "description": "Resource", + "operationId": "getPropertiesCorrect", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "OK", + "content": { + "application/problem+json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "integer" }, + "title": { "type": "string" }, + "detail": { "type": "string" }, + "errors": { + "type": "object", + "properties": { + "in": { "type": "string" }, + "location": { + "type": "object", + "properties": { + "pointer": { "type": "string" }, + "name": { "type": "string" }, + "index": { "type": "integer" } + } + }, + "code": { "type": "string" }, + "detail": { "type": "string" }, + "extra": { "type": "string" } + } + } + }, + "required": ["status", "title", "detail", "errors"], + "additionalProperties": false + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + }, + "/properties-zonder-location": { + "get": { + "tags": [ + "resource" + ], + "description": "Resource", + "operationId": "getResource", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "OK", + "content": { + "application/problem+json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "integer" }, + "title": { "type": "string" }, + "detail": { "type": "string" }, + "errors": { + "type": "object", + "properties": { + "in": { "type": "string" }, + "code": { "type": "string" }, + "detail": { "type": "string" } + } + } + }, + "required": ["status", "title", "detail", "errors"], + "additionalProperties": false + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + }, + "/properties-location-verkeerd-format": { + "get": { + "tags": [ + "resource" + ], + "description": "Resource", + "operationId": "getPropertiesVerkeerdFormat", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "OK", + "content": { + "application/problem+json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "integer" }, + "title": { "type": "string" }, + "detail": { "type": "string" }, + "errors": { + "type": "object", + "properties": { + "in": { "type": "string" }, + "location": { + "type": "object", + "properties": { + "pointer2": { "type": "string" }, + "name": { "type": "string" }, + "index": { "type": "integer" } + } + }, + "code": { "type": "string" }, + "detail": { "type": "string" } + } + } + }, + "required": ["status", "title", "detail", "errors"], + "additionalProperties": false + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + } + }, + "components": { + "schemas": { + }, + "securitySchemes": { + "default": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://test.com", + "scopes": {} + } + } + } + } + } +} \ No newline at end of file diff --git a/linter/testcases/error-type-extra-field/expected-output.txt b/linter/testcases/error-type-extra-field/expected-output.txt new file mode 100644 index 0000000..95cc954 --- /dev/null +++ b/linter/testcases/error-type-extra-field/expected-output.txt @@ -0,0 +1 @@ +No results with a severity of 'error' found! diff --git a/linter/testcases/error-type-extra-field/openapi.json b/linter/testcases/error-type-extra-field/openapi.json new file mode 100644 index 0000000..d1917c2 --- /dev/null +++ b/linter/testcases/error-type-extra-field/openapi.json @@ -0,0 +1,96 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Baseline", + "description": "Deze OpenAPI specification bevat het minimale om aan alle regels te voldoen.", + "contact": { + "name": "Beheerder", + "url": "https://www.example.com", + "email": "mail@example.com" + }, + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://example.com/api/v1" + } + ], + "security": [ + { + "default": [] + } + ], + "tags": [ + { + "name": "openapi" + } + ], + "paths": { + "/openapi.json": { + "get": { + "tags": [ + "openapi" + ], + "description": "OpenAPI document", + "operationId": "getOpenapiJSON", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + }, + "access-control-allow-origin": { + "description": "Alle origins mogen bij deze resource", + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "NOK", + "content": { + "application/problem+json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "integer" }, + "title": { "type": "string" }, + "detail": {"type": "string" }, + "extra": {"type": "string" } + } + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + } + }, + "components": { + "schemas": { + }, + "securitySchemes": { + "default": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://test.com", + "scopes": {} + } + } + } + } + } +} \ No newline at end of file diff --git a/linter/testcases/error-type-missing-required/expected-output.txt b/linter/testcases/error-type-missing-required/expected-output.txt new file mode 100644 index 0000000..60a8b61 --- /dev/null +++ b/linter/testcases/error-type-missing-required/expected-output.txt @@ -0,0 +1,5 @@ + +/testcases/error-type-missing-required/openapi.json + 62:50 error nlgov:problem-schema-members "properties" property must have required property "detail". These fields are required: status, title and detail. paths./openapi.json.get.responses[404].content.application/problem+json.schema.properties + +✖ 1 problem (1 error, 0 warnings, 0 infos, 0 hints) diff --git a/linter/testcases/error-type-missing-required/openapi.json b/linter/testcases/error-type-missing-required/openapi.json new file mode 100644 index 0000000..bf748b3 --- /dev/null +++ b/linter/testcases/error-type-missing-required/openapi.json @@ -0,0 +1,96 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Baseline", + "description": "Deze OpenAPI specification bevat het minimale om aan alle regels te voldoen.", + "contact": { + "name": "Beheerder", + "url": "https://www.example.com", + "email": "mail@example.com" + }, + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://example.com/api/v1" + } + ], + "security": [ + { + "default": [] + } + ], + "tags": [ + { + "name": "openapi" + } + ], + "paths": { + "/openapi.json": { + "get": { + "tags": [ + "openapi" + ], + "description": "OpenAPI document", + "operationId": "getOpenapiJSON", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "description": "De huidige versie van de applicatie", + "style": "simple", + "schema": { + "type": "string" + } + }, + "access-control-allow-origin": { + "description": "Alle origins mogen bij deze resource", + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "NOK", + "content": { + "application/problem+json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "integer" }, + "title": { "type": "string" } + }, + "required": ["status", "title"], + "additionalProperties": false + } + } + } + } + }, + "security": [ + { + "default": [] + } + ] + } + } + }, + "components": { + "schemas": { + }, + "securitySchemes": { + "default": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://test.com", + "scopes": {} + } + } + } + } + } +} \ No newline at end of file diff --git a/linter/testcases/error-type/expected-output.txt b/linter/testcases/error-type/expected-output.txt index f2893cd..c79b49a 100644 --- a/linter/testcases/error-type/expected-output.txt +++ b/linter/testcases/error-type/expected-output.txt @@ -1,5 +1,5 @@ /testcases/error-type/openapi.json - 58:35 warning nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./openapi.json.get.responses[400].content + 58:35 error nlgov:use-problem-schema The content type of an error response should be application/problem+json or application/problem+xml to match RFC 9457. paths./openapi.json.get.responses[404].content -✖ 1 problem (0 errors, 1 warning, 0 infos, 0 hints) +✖ 1 problem (1 error, 0 warnings, 0 infos, 0 hints) diff --git a/linter/testcases/error-type/openapi.json b/linter/testcases/error-type/openapi.json index 4f28a88..176c0d3 100644 --- a/linter/testcases/error-type/openapi.json +++ b/linter/testcases/error-type/openapi.json @@ -53,7 +53,7 @@ } } }, - "400": { + "404": { "description": "OK", "content": { "application/hal+json": { diff --git a/linter/testcases/paths-kebab-variables/expected-output.txt b/linter/testcases/paths-kebab-variables/expected-output.txt index 95cc954..2b6ff07 100644 --- a/linter/testcases/paths-kebab-variables/expected-output.txt +++ b/linter/testcases/paths-kebab-variables/expected-output.txt @@ -1 +1,7 @@ -No results with a severity of 'error' found! + +/testcases/paths-kebab-variables/openapi.json + 87:29 error nlgov:problem-invalid-input GET endpoints that have parameters and all other endpoints must be able to return a 400 response paths./organisaties/{id}.get.responses + 128:29 error nlgov:problem-invalid-input GET endpoints that have parameters and all other endpoints must be able to return a 400 response paths./organisaties/{id}/nested.get.responses + 169:29 error nlgov:problem-invalid-input GET endpoints that have parameters and all other endpoints must be able to return a 400 response paths./organisaties/{organisationId}/pad.get.responses + +✖ 3 problems (3 errors, 0 warnings, 0 infos, 0 hints) diff --git a/linter/testcases/paths-kebab-zoek-uitzondering/expected-output.txt b/linter/testcases/paths-kebab-zoek-uitzondering/expected-output.txt index d1c1822..951ad9d 100644 --- a/linter/testcases/paths-kebab-zoek-uitzondering/expected-output.txt +++ b/linter/testcases/paths-kebab-zoek-uitzondering/expected-output.txt @@ -1,5 +1,6 @@ /testcases/paths-kebab-zoek-uitzondering/openapi.json - 125:19 warning path-keys-no-trailing-slash Path must not end with slash. paths./_zoek/ + 125:19 warning path-keys-no-trailing-slash Path must not end with slash. paths./_zoek/ + 174:29 error nlgov:problem-invalid-input GET endpoints that have parameters and all other endpoints must be able to return a 400 response paths./organisaties/{id}/nested/_zoek.get.responses -✖ 1 problem (0 errors, 1 warning, 0 infos, 0 hints) +✖ 2 problems (1 error, 1 warning, 0 infos, 0 hints) diff --git a/linter/testcases/query-keys-camel-case/expected-output.txt b/linter/testcases/query-keys-camel-case/expected-output.txt index 3e99836..228c642 100644 --- a/linter/testcases/query-keys-camel-case/expected-output.txt +++ b/linter/testcases/query-keys-camel-case/expected-output.txt @@ -1,9 +1,10 @@ /testcases/query-keys-camel-case/openapi.json - 84:33 error nlgov:query-keys-camel-case kebab-case is not lower camelCase. paths./resource.get.parameters[1].name - 91:33 error nlgov:query-keys-camel-case _startMetUnderscore is not lower camelCase. paths./resource.get.parameters[2].name - 98:33 error nlgov:query-keys-camel-case 9startMetGetal is not lower camelCase. paths./resource.get.parameters[3].name - 105:33 error nlgov:query-keys-camel-case snake_case is not lower camelCase. paths./resource.get.parameters[4].name - 112:33 error nlgov:query-keys-camel-case UpperCamelCase is not lower camelCase. paths./resource.get.parameters[5].name + 84:33 error nlgov:query-keys-camel-case kebab-case is not lower camelCase. paths./resource.get.parameters[1].name + 91:33 error nlgov:query-keys-camel-case _startMetUnderscore is not lower camelCase. paths./resource.get.parameters[2].name + 98:33 error nlgov:query-keys-camel-case 9startMetGetal is not lower camelCase. paths./resource.get.parameters[3].name + 105:33 error nlgov:query-keys-camel-case snake_case is not lower camelCase. paths./resource.get.parameters[4].name + 112:33 error nlgov:query-keys-camel-case UpperCamelCase is not lower camelCase. paths./resource.get.parameters[5].name + 118:29 error nlgov:problem-invalid-input GET endpoints that have parameters and all other endpoints must be able to return a 400 response paths./resource.get.responses -✖ 5 problems (5 errors, 0 warnings, 0 infos, 0 hints) +✖ 6 problems (6 errors, 0 warnings, 0 infos, 0 hints) diff --git a/sections/designRules.md b/sections/designRules.md index 60c76cb..43b4afb 100644 --- a/sections/designRules.md +++ b/sections/designRules.md @@ -214,6 +214,93 @@ https://api.example.org/v1/vergunningen/d285e05c-6b01-45c3-92d8-5e19a946b66f +## Date and time + +Handling date and time is tricky and can lead to confusion among clients. The date-time rules remove ambiguity and provide clarity in the API contract between servers and clients. + + + +
Use standard format for date, datetime and time
+All date, datetime and time fields in requests and responses MUST adhere to [[RFC9557]] and [[ISO8601]] format. Each field in the OpenAPI specification MUST set "type": "string" and set "format" to the OpenAPI format as listed in the following table:
+
| Field type | +ISO8601 format | +OpenAPI format | +
|---|---|---|
| Date | +full-date (YYYY-MM-DD) |
+ "format": "date" |
+
| Datetime | +date-time (YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss±hh:mm) |
+ "format": "date-time" |
+
| Time | +time (hh:mm:ss) |
+ "format": "time-local" |
+
Implementing RFC9557 and ISO 8601 removes ambiguity in date handling between systems and timezones. +
Allow all timezone offsets in requests and use UTC in responses
+APIs MUST accept any timezone offset in fields in requests containing a datetime. Fields in responses containing a datetime MUST be in UTC (e.g. Z as timezone offset).
+
Allowing clients to use any timezone offset in requests results in flexibility and less complexity for users. Using UTC in responses results in clarity and removes ambiguity. +
This specification does not state rules regarding storage in databases. + However, if the original timezone of a given timestamp value is relevant for users (such as the timezone in which a value is registered), it is recommended to store and publish the timezone details (e.g. the zone offset) as a separate property. +
Omit time portion for date fields
+If the time portion is not relevant, date format MUST be used instead of date-time format.
+
Appending a default or irrelevant time portion to a date field can lead to interpretation errors. A publish date of 2025-07-24T00:00:00Z could for instance be rendered as July 23 in Ireland. A default time of 23:59 would in turn cause date confusion east of Greenwich.
+
Use problem details for error responses
+Error responses with HTTP status codes 4xx or 5xx MUST use either application/problem+json or application/problem+xml as the Content-Type header, and the response body MUST conform to the structure defined in [[rfc9457]].
+
The following fields MUST be present: status, title, and detail.
+
Providing problem details in a machine-readable format aids automation and debugging. By using a common error format, APIs do not need to define their own or misuse existing HTTP status codes.
+ +4xx or 5xx have Content-Type set to application/problem+json or application/problem+xml and the body contains fields status, title, and detail.
+ Use status code 400 for invalid input
+API requests containing invalid input MUST result in HTTP status code 400 (Bad Request).
+ Invalid input includes syntax errors, missing or invalid query parameters.
+
The request payload SHOULD be validated with a schema. A request payload with schema validation errors MUST be treated as invalid input. +
The semantics of status code 400 ("the server cannot or will not process the request due to something that is perceived to be a client error") match validation failures more closely than status code 422, which historically originates from WebDAV and introduces no added interoperability benefit.
400.
+ Add specific errors for Bad Request responses
+Problem details with status code 400 (Bad Request) MUST include an additional member errors containing an ordered list of validation error objects, as specified below.
+
Each error object MUST contain in and detail members, and MAY optionally contain location, index and code members.
+
in - where the error occurs: body or query.detail - a human-readable message describing the violation.location (optional) - a locator for the offending value:
+ body errors, the location member may be omitted, in case the error refers to the body as a whole (e.g. syntax errors).
+ index (optional) - a zero-based index position when multiple query parameters have the same name.code (optional) - a short, stable machine-readable code as a rule identifier (e.g. date.format). If a type URI is provided on the message-level, dereferencing this URI SHOULD result in a page describing all possible code values including a description for each value.Having a single, consistent errors structure makes validation issues predictable for clients, while relying on established locators using universal standards (JSON Pointer, XPath).
400 contain a required errors member conforming to the requirements above.
+ Return all errors together for bad requests
+API requests with HTTP status code 400 (Bad Request) SHOULD include all applicable schema validation errors and MAY include additional errors.
+
To reduce the amount of roundtrips between client and server, all applicable schema validation errors SHOULD be returned together. + This allows a client to present validation errors to a user in one go, reducing user friction with multiple retries. +
It depends on a validation technique whether this is possible or not. + For example, when a client provides a date in the weekend, where only dates on weekdays are allowed, it depends on which service performs these validation checks. + In these cases, include the additional validation errors together with other errors whenever feasible. +