diff --git a/linter/spectral.yml b/linter/spectral.yml index da7089a..97c7d2d 100644 --- a/linter/spectral.yml +++ b/linter/spectral.yml @@ -206,6 +206,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..95360a1 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 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 + 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 (2 errors, 23 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/sections/designRules.md b/sections/designRules.md index 60c76cb..94a1241 100644 --- a/sections/designRules.md +++ b/sections/designRules.md @@ -214,6 +214,100 @@ 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

+
+
Statement
+
+

All date, datetime and time fields in requests and responses MUST adhere to [[RFC9557]] and [[ISO8601-1]] 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 typeISO8601 formatOpenAPI format
Datefull-date"format": "date"
Datetimedate-time"format": "date-time"
Timetime"format": "time-local"
+

+
Rationale
+
+

Implementing RFC9557 and ISO 8601 removes ambiguity in date handling between systems and timezones. +

RFC9557 is a profile on ISO8601, but is not a strict subset of allowed notations. Practically to adhere to both, the following limitations MUST be applied to RFC9557: +
    +
  • In a field with a date-time value, the date and time component MUST be separated by a "T" in uppercase. +
  • The timezone offset "Z" MUST be uppercase. +
  • "-00:00" MUST NOT be used as timezone offset. +
+
+
+
How to test
+
+ Analyse all fields and if the field represents a date, date-time or time, ensure it has the correct format according to the table above. +
+
+
+ +
+

Allow all timezone offsets in requests and use UTC in responses

+
+
Statement
+
+

APIs MUST accept any timezone offset in fields in requests containing a datetime. Fields in responses containing a datetime SHOULD be in UTC (e.g. Z as timezone offset). +

+
Rationale
+
+

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. +

While storage formats are outside the scope of this specification, it is recommended to use appropriate temporal datatypes (such as DATE and TIMESTAMPTZ). Many database systems store these values internally in UTC and handle timezone conversion automatically on read/write. When the original timezone is relevant for users (such as the timezone in which a value is registered), store and publish the zone offset as a separate property. +

+
+
+ +
+

Omit time portion for date fields

+
+
Statement
+
+

If the time portion is not relevant, date format MUST be used instead of date-time format. +

+
Rationale
+
+

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. +

To prevent conversion errors between storage and API, it is recommended to also store date values without a time portion (i.e. not as a timestamp). +

+
How to test
+
+ Analyse all fields that set format to "date-time" and ensure that the fields do not represent solely a date. +
+
+
+ ## HTTP methods Although the REST architectural style does not impose a specific protocol, REST APIs are typically implemented using HTTP [[rfc9110]].