Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions linter/testcases/return-objects/expected-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

/testcases/return-objects/openapi.json
108:44 error nlgov:always-return-objects The content of all responses should be wrapped in an object paths./incorrect.get.responses[200].content.application/json.schema.type
113:44 error nlgov:always-return-objects The content of all responses should be wrapped in an object paths./incorrect.get.responses[200].content.application/hal+json.schema.type
118:44 error nlgov:always-return-objects The content of all responses should be wrapped in an object paths./incorrect.get.responses[200].content.application/xml.schema.type

✖ 3 problems (3 errors, 0 warnings, 0 infos, 0 hints)
147 changes: 147 additions & 0 deletions linter/testcases/return-objects/openapi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
{
"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": {
"/correct": {
"get": {
"tags": [
"openapi"
],
"description": "OpenAPI document",
"operationId": "getCorrect",
"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"
}
},
"application/hal+json": {
"schema": {
"type": "object"
}
},
"application/xml": {
"schema": {
"type": "object"
}
},
"text/plain": {
"schema": {
"type": "string"
}
},
"text/html": {
"schema": {
"type": "string"
}
}
}
}
},
"security": [
{
"default": []
}
]
}
},
"/incorrect": {
"get": {
"tags": [
"openapi"
],
"description": "OpenAPI document",
"operationId": "getIncorrect",
"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": "array"
}
},
"application/hal+json": {
"schema": {
"type": "array"
}
},
"application/xml": {
"schema": {
"type": "array"
}
}
}
}
},
"security": [
{
"default": []
}
]
}
}
},
"components": {
"schemas": {
},
"securitySchemes": {
"default": {
"type": "oauth2",
"flows": {
"implicit": {
"authorizationUrl": "https://test.com",
"scopes": {}
}
}
}
}
}
}
11 changes: 11 additions & 0 deletions media/linter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,14 @@ rules:
function: pattern
functionOptions:
match: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$

#/core/always-return-objects
nlgov:always-return-objects:
severity: error
message: "The content of all responses should be wrapped in an object"
given: $..[responses][*][content][?(@property.toString().match(/(json)|(xml)/))][schema]
then:
field: type
function: pattern
functionOptions:
match: "object"
47 changes: 47 additions & 0 deletions sections/designRules.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,53 @@ https://api.example.org/v1/vergunningen/d285e05c-6b01-45c3-92d8-5e19a946b66f</pr
</dl>
</div>

<div class="rule" id="/core/always-return-objects" data-type="technical">
<p class="rulelab">Always return a response with an object</p>
<dl>
<dt>Statement</dt>
<dd>
<p>A JSON or XML response MUST have a top-level object, regardless of request method.
For <a>collection resources</a>, the object MUST contain a field (its key MAY be named <code>items</code>) with its value an array of items from that collection.
Comment thread
TimvdLippe marked this conversation as resolved.
</dd>
<dt>Rationale</dt>
<dd>
<p>For <a>singular resources</a>, objects are returned as part of RESTful design.
<p>For <a>collection resources</a>, items can be retrieved all at once, or a subset thereof.
In both cases, wrapping the returned items in an object allows metadata to be attached to the response.
Examples of metadata are structured data to implement linked data concepts, or pagination data for performance purposes.
<p>If a collection resource directly returns an array of object information from the collection, such metadata can only be provided using HTTP headers.
While that is feasible, HTTP headers are generally more difficult to work with than content from a response body.
<p>Even if currently no metadata is attached to a response, that could be the case in the future.
Changing the response from an array to an object is a breaking change, hence it is future-proof to always return an object.
<aside class="example">
The following example shows a response from a collection resource containing all items
<pre class="http">GET /organisations HTTP/1.1</pre>
<pre class="http">HTTP/1.1 200 OK
Content-Type: application/json

{
"metadata": {
},
"items": [
{
"id": "12345",
"name": "Logius"
},
{
"id": "67890",
"name": "Geonovum"
}
]
}</pre>
</aside>
</dd>
<dt>How to test</dt>
<dd>
Analyse all JSON and XML responses for paths and check that the response contains a top-level object.
</dd>
</dl>
</div>

<span id="api-53"></span>
<div class="rule" id="/core/hide-implementation" data-type="functional">
<p class="rulelab">Hide irrelevant implementation details</p>
Expand Down
Loading