API specifications
Rule: API contract-first approach [ctr-first]
The contract-first principle SHOULD be followed when developing an API. In this approach, the specifications of the REST API are created first and not generated from the code.
A code-first approach may sometimes be necessary however, as current state of technology does not always fully support contract-first development. In this case, special attention should be given that the API specifications remain stable and loosely coupled to the API implementation technology.
OpenAPI
OpenAPI is the dominating standard for REST API documentation, having gained wide industry support. It is based on Swagger, which was standardized as OpenAPI.
Rule: OpenAPI [oas-contra]
The description of a new REST API SHOULD be provided using OpenAPI 3.0. OpenAPI uses the OpenAPI Schema Object to describe the JSON representation of resources, which is a variant of JSON Schema, with some significant incompatibilities.
More recent OpenAPI 3.x versions improve upon OpenAPI 3.0, but SHOULD NOT be used to avoid interoperability problems because they are not yet sufficiently supported by tooling.
The OpenAPI standard also includes a list of supported primitive data types.
You can visualize OpenAPI documents using some of the tools listed in Tools.
Rule: Use YAML format for OpenAPI [oas-yaml]
OpenAPI documents SHOULD be made available in YAML format, and OPTIONALLY in JSON format as well.
YAML is a superset of JSON which allows a simpler notation, and also allows comments. According to usage, a conversion step to JSON might be necessary considering limitations of tools.
Rule: OpenAPI as documentation [oas-doc]
OpenAPI authors SHOULD add sufficient documentation so that the OpenAPI can be used as an entry point to start using the API. Documentation SHOULD be geared towards users, and SHOULD NOT document the internal implementation of the API.
Alongside the definitions of allowed interactions (operations, schemas, …), sufficient additional documentation SHOULD be provided by:
-
Adding clarifications in various free-text documentation properties ([oas-descr])
-
Using tags to group operations ([oas-tags])
-
Adding examples throughout the OpenAPI ([oas-exampl])
-
Linking to any documentation not part of the OpenAPI, using the
externalDocsproperty.
Rule: Free-text documentation [oas-descr]
Free-text documentation can be added throughout an OpenAPI:
-
General documentation for the whole API can be provided in the OpenAPI’s
infoobject, liketitle(REQUIRED),contactanddescription -
The
summaryproperty SHOULD be present on each operation providing a short functional description. When using OpenAPI visualization tools like SwaggerUI, it is displayed in the overview of all operations. -
The
descriptionproperty on various definitions MAY be used to provide longer descriptions-
All
descriptionproperties support CommonMark Markdown format for rich text formatting.
-
-
The
titleproperty on schemas MAY be used to provide a human-readable short name of a schema-
For reusable schemas under
components, it is redundant with the component’s name. In this case, it either SHOULD NOT be used or have the same value as the component’s name so it won’t hide it in visualization tools like SwaggerUI. -
For inline schemas (i.e. part of another definition) of type
object, an UpperCamelCasetitleSHOULD be used to provide a human-readable name for code generation. However:-
if the schema can be reused, it is better to refactor it to its own schema component ([oas-comp]).
-
for inline schemas under the
allOfornotkeywords, thetitleproperty is OPTIONAL, because no classes are generated for them.
-
-
GOOD
components:
schemas:
Pet:
description: a pet in the pet store
type: object
components:
schemas:
Pet:
title: Pet # OK, because identical to the schema component's name; can also be omitted
description: a pet in the pet store
type: object
components:
schemas:
Pet:
type: object
description: a pet in the pet store
properties:
owner:
title: Owner # OK. The title is used for code generation and visualization. If the Owner schema can be reused, move it to its own schema component and reference it
description: The owner of the pet
type: object
BAD
components:
schemas:
Pet:
title: a pet in the pet store # title overrides the Pet schema name in visualization tooling
type: object
Rule: Use tags to group operations [oas-tags]
When an API has many operations, tags SHOULD be used to group them together.
An operation SHOULD NOT have more than one tag.
Each tag used on an operation SHOULD also be declared in the top level tags list of the OpenAPI document, with an optional description.
The name of a tag SHOULD be stylized like a title, i.e. a word or space-separated word group starting with a capital letter.
Using tags will make the visual representation (e.g. SwaggerUI) more readable. Multiple tags on an operation duplicates the operation in SwaggerUI, which may cause confusion.
Declaring each tag allows linting tooling to detect inconsistencies in tag names.
tags:
- name: Employers
description: Managing employers
- name: Reference data
paths:
/employers/{employerId}:
get:
summary: Get an employer by its identifier
tags:
- Employers
/employers:
get:
summary: Find employers matching given search parameters
tags:
- Employers
post:
summary: Add a new employer
tags:
- Employers
/refData/employerClasses:
get:
summary: Find enterprises matching given search parameters
tags:
- referenceData # BAD: doesn't match declared tag name, words aren't separated by space, doesn't start with capital letter
- Employers # BAD: don't set multiple tags on an operation
Rule: operationId [openapi-opid]
A unique operationId MUST be specified on each operation.
It SHOULD have a lowerCamelCase value following common programming naming conventions for method (function) names.
Tools and libraries often use the operationId to uniquely identify an operation.
Code generators use it to generate method names and generally take care of transforming it to the language-specific naming conventions (e.g. UpperCamelCase or snake_case).
/health:
get:
summary: Check health of the service
operationId: checkHealth # Bad values: health, CheckHealth, healthCheck
Rule: Required request payload [opbody-req]
Request payloads are optional by default in OpenAPI, even though they usually need to be required. It is most of the time an omission if this default wasn’t changed.
OpenAPI authors MUST specify the required property on a requestBody explicitly, to avoid any ambiguity.
/documents:
post:
summary: Create a new document
operationId: createDocument
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Document"
required: true
Rule: Security requirements [sec-req]
The security field defined at the root level of the OpenAPI entry document, or on an operation object, can be used to specify security requirements for interactions with the API.
Each property name under security MUST correspond to a security scheme defined in #/components/securitySchemes in the entry OpenAPI document. If the security scheme is of type oauth2 or openIdConnect, the value is a list of scope names required for the execution, and the list MAY be empty if authorization does not require a specified scope.
For oauth2 security schemes, the scope names in the security requirement MUST correspond to one of the scopes values in one of the OAuth flows of the corresponding security scheme component.
security: # root-level security requirement is applied to all operations unless overridden
- Oauth: # the name of the securityScheme component
- 'scope:organization:myapi:consult' # scope name in at least one of the flows of the securityScheme
- Oauth: # the name of the securityScheme component
- 'scope:organization:myapi:update' # scope name in at least one of the flows of the securityScheme
components:
securitySchemes:
Oauth: # the name of the securityScheme component
type: oauth2
flows:
authorizationCode:
authorizationUrl: 'https://oauth.example.org/authorize'
tokenUrl: 'https://oauth.example.org/token'
scopes:
'scope:organization:myapi:consult': this scope allows to consult all data
'scope:organization:myapi:update': this scope allows to update all data
clientCredentials:
tokenUrl: 'https://oauth.example.org/token'
scopes:
'scope:organization:myapi:consult': this scope allows to consult all data
'scope:organization:myapi:update': this scope allows to update all data
Rule: General HTTP headers [gen-header]
HTTP header parameters that may be present on all or most operations of an API SHOULD NOT be documented explicitly in the OpenAPI description, as this would clutter the spec and make it difficult to keep up to date. Rather, refer API users to general documentation on these headers.
Operation-specific HTTP header parameters SHOULD be documented.
Following are examples of HTTP headers that shouldn’t be documented explicitly for each operation:
-
Accept-Language -
BelGov-Trace-IdandBelGov-Related-Trace-Id(documented in Tracing) -
Authorization
Examples of headers that should be specified on operations that support them:
-
ETag -
If-Modified-Since
Rule: doc and refData resources [doc-refdata]
The OpenAPI description and other API documentation SHOULD be placed under a special doc resource under the API root location.
Access to these documentation resources SHOULD be granted to any user or potential user of the API.
The OpenAPI 3.0 entry document SHOULD be named openapi.json or openapi.yaml, as specified in the OpenAPI 3.0 specification.
Resources representing reference data (code lists) specific to an API SHOULD be placed under a refData resource.
As reference data is typically static, consider supporting caching headers for these resources.
Note that these are exceptions to the rule that API resource URLs shouldn’t have a file extension.
doc and refData resources /doc
/openapi.json
/openapi.yaml
/<optional other documentation>
/refData
/<list1OfCodes>
/<code1>
/<code2>
/...
/<list2OfCodes>
/...
/<resource1>
/...
/<resource2>
/...
...
Rule: default values [oas-defaul]
Absent optional properties in a request are set by the API provider to their default value if one is specified in the OpenAPI description.
Default values MUST be schema-valid.
Rule: Naming of path parameters [path-param]
Path parameters SHOULD use lowerCamelCase notation.
| GOOD | BAD |
|---|---|
|
|
Rule: Add examples in OpenAPI [oas-exampl]
Add examples to an OpenAPI document.
Examples SHOULD be provided under the example or examples OpenAPI properties rather than in a description. examples allows specifying multiple examples, but can’t be used within schema definitions.
Example values MUST be schema-valid.
/enterprises/{enterpriseNumber}:
get:
operationId: getEnterprise
parameters:
- in: path
name: enterpriseNumber
required: true
schema:
type: string
responses:
"200":
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Enterprise'
examples:
success:
description: Successful response with enterprise data
value: {
"name": "Proximus",
"enterpriseNumber": "0202239951"
}
Rule: Component definitions [oas-comp]
Duplication of definitions (schemas, responses, parameters, etc.) SHOULD be avoided.
Rather, define them once under components, from which they can be referenced (using $ref).
A $ref SHOULD always reference a component of the expected type under /components/<componentTypes>/<componentName>, except for path items which can reference /paths/<pathName>.
All component names SHOULD be defined in American English and use UpperCamelCase notation. For abbreviations as well, all letters except the first one should be lowercased.
Do not use underscores (_), hyphens (-) or dots (.) in a component name, nor use a digit as first letter.
Only from OpenAPI 3.1 onwards, a /components/pathItems section is supported. For OpenAPI 3.0, reusable path items can only be defined under /paths. To reference them, forward slashes need to be escaped using ~1 e.g.
paths:
"/organizations/{enterpriseNumber}":
"$ref": "./my-paths.yaml#/paths/~1organizations~1{enterpriseNumber}"
Rule: Reusable OpenAPI documents [oas-reuse]
Instead of specifying everything directly in the openapi.yaml file of an API, OpenAPI allows to reference schemas (data types) and other components from other files.
These files SHOULD be OpenAPI documents as well that follow the OpenAPI specification and may include schema definitions, but also other component types like parameters, path items, request bodies and responses.
To work around limitations of certain tools, a conversion step to inline the definitions into the openapi.yaml entry document may be necessary.
Duplication of definitions in multiple APIs SHOULD be avoided. Rather, put the definition in a reusable OpenAPI document. OpenAPI documents reusable from multiple APIs SHOULD be organized in this structure:
<domain>/<version>/<domain-version>.yaml
<domain>/<subdomain>/<version>/<domain-subdomain-version>.yaml
Definitions SHOULD be grouped per (sub)domain in an OpenAPI document.
Each OpenAPI document has its own lifecycle, with a major version number in its directory and file name, that is increased when backwards compatibility is broken.
This version, with optionally a minor and patch version added to it, MUST be specified in the info section in the OpenAPI document.
While it is not strictly necessary for external definitions to be put in a valid OpenAPI document, doing so makes it possible to use standard OpenAPI tooling on them.
openapi: "3.0.3"
info:
title: person-identifier
description: data types for person identifiers
version: "1.1.2"
paths: {} # empty paths property required to be a valid OpenAPI document
components:
schemas:
Ssin:
description: "Social Security Identification Number issued by the National Register or CBSS"
type: string
pattern: \d{11}
A type can be referenced from another OpenAPI document:
"$ref": "./person/identifier/v1/person-identifier-v1.yaml#/components/schemas/Ssin"
Rule: Common definitions for Belgian government institutions [oas-comdef]
Common definitions for use by Belgian government institutions are maintained in the openapi-* GitHub repositories, released as zip archives, organized per domain and also available in Maven Central. Types in these schemas SHOULD be used instead of defining your own variants.
The technical types referenced in this style guide are available in the openapi-common and openapi-problem repositories. Other types for business concepts commonly used by Belgian government institutions are available in other repositories.
Schemas
Rule: Naming of schemas (data types) [oas-types]
As stated in [oas-comp], schema (data type) names should use UpperCamelCase and American English.
In addition, schema names SHOULD NOT include overly generic terms like info(rmation) and data.
A schema name SHOULD refer to the business meaning rather than how it is defined.
| KO | OK |
|---|---|
SSIN |
Ssin |
CustomerInformation |
Customer |
LanguageEnumeration |
Language |
additionalProperties can be used to put restrictions on other properties of a JSON object than those specified in the schema.
Rule: additionalProperties [addi-prop]
additionalProperties SHOULD be used exclusively to describe an object uniquely representing a map of key-value pairs.
The keys of such maps don’t need to respect the naming rules for JSON properties (lowerCamelCase and English).
Other uses of additionalProperties (e.g. additionalProperties: false) SHOULD be avoided, in order to support API evolution ([evo-compat]).
An example is the description a map of embedded resources, as described in Embedding.
Rule: Required properties [prop-req]
You can declare properties of a JSON object as mandatory by adding them to the required list.
Any properties listed in required MUST also be declared in the object’s properties, either inline or in a referenced subschema.
While undeclared required properties are permitted by the JSON Schema standard, they are most often the result of an error in the OpenAPI document (typo, oversight when renaming a property, …). This rule allows linting tools to mark such errors.
| GOOD | BAD |
|---|---|
|
|
Rule: Unknown input [req-valid]
An API SHOULD refuse unknown input, i.e. unknown body (JSON) properties in the payload or unknown query request parameters. A Bad Request problem response should be returned with an issue of type urn:problem-type:belgif:input-validation:unknownInput.
If an operation does allow and process input that is not defined in OpenAPI, its description should explicitly indicate this.
In specific situations, where a (known) input is not needed anymore and can be safely ignored:
-
either it can stay in the API definition with a deprecation flag and a "not used anymore" description
-
or it can be removed from the API definition as long as the server ignores this specific input.
Unknown HTTP header parameters MUST be accepted.
If unknown request fields would be accepted, certain client errors cannot be recognized by servers, e.g. parameter name typing errors will be ignored and the client’s actual intent will not be met.
Unknown HTTP headers are usually metadata added automatically by technical components that do not change the API’s expected behavior and thereby can be ignored.
Rule: readOnly properties [oas-rdonly]
Properties SHOULD be declared as readOnly: true when appropriate.
This means that the value of the property is managed exclusively by the owning authority, and attempts by a client to modify the value of this property are expected to be ignored or rejected.
Examples are an immutable identifier of a document, properties that are computed from other properties, or that represent a volatile state of a resource.
Properties marked as readOnly being true SHOULD NOT be in the required list of the defined schema.
Above guideline is compliant with the definition of readOnly used by the OpenAPI 3.1 Specification.
The OpenAPI 3.0 Specification has a different definition, that allows to omit properties from requests that are both readOnly and required. This has been changed in OpenAPI 3.1 to achieve full compatibility with the JSON Schema Specification. Hence, to be forward-compatible, OpenAPI documents should not depend on this 3.0 behavior.
Rule: Enum values [oas-enum]
A fixed list of possible values of a property can be specified using enum.
However, this may make it harder to change the list of possible values, as client applications will often depend on the specified list e.g. by using code generation.
enum SHOULD only be used when the list of values is unlikely to change or when changing it has a big impact on clients of the API.
enum values SHOULD be valid to the schema in which they are defined.
State:
type: string
enum:
- processing
- failed
- done
When defining a type for an identifier or code, like the above example, the guidelines under Identifier apply, even when not used as a URL path parameter of a document resource.
Rule: Decimals [dec-type]
Decimal numbers for which the fractional part’s precision is important, like monetary amounts, SHOULD be represented by a string-based type, with number as format. Depending on the context, a regular expression can enforce further restrictions like the number of digits allowed before/after comma or on the presence of a +/- sign.
When number would be used as type instead of string, some technologies will convert the values to floating point numbers, leading to a loss of precision and unintended calculation errors.
This problem may also be avoided by using an equivalent integer representation, for example by expressing a monetary amount in Euro cent rather than Euro.
Some more background on why floating point numbers can lead to loss of precision, can be found in this blog post.
belgif openapi-money defines a string-based type for monetary values:
MonetaryValue:
type: string
format: number # number is a custom string format that is supported by some, but not all tooling
pattern: '^(\-|\+)?((\d+(\.\d*)?)|(\.\d+))$' # Variable number of digits, with at least one digit required, before or after the decimal point. Allows both positive and negative values.
x-examples:
- "100.234567"
- "010"
- "-.05"
- "+1"
- "10"
- "100."
MonetaryAmount:
description: A monetary amount
type: object
properties:
value:
"$ref": "#/components/schemas/MonetaryValue"
currency:
"$ref": "#/components/schemas/Currency"
required: [value, currency]
example:
value: "0.01"
currency: "EUR"
It also defines integer-based types specific for monetary amounts expressed in Euro cent:
EuroCentPositiveAmount:
description: Money amount in Euro cents >= 0
type: integer # representation as Euro cent instead of Euro to avoid floating point rounding problems and need for custom 'number' format
minimum: 0
EuroCentAmount:
description: 'Money amount in Euro cents, also allows negative amounts.'
type: integer # representation as Euro cent instead of Euro to avoid floating point rounding problems and need for custom 'number' format
While the OpenAPI Specification is well suited to document APIs and express constraints on requests and responses, the full expressiveness of OpenAPI schemas may cause interoperability problems with the type system of common programming languages. This guide adds some restrictions on the use of OpenAPI schemas to avoid such problems.
Rule: Composing schemas with allOf [sch-allof]
The allOf JSON Schema keyword expresses that a value must be valid against all of a list of subschemas.
For object types, it can be used to model an inheritance relationship between schemas. For code generation, the first subschema that’s a reference ($ref) is considered the parent.
Subschemas in an allOf SHOULD NOT declare the same properties, because this will lead to problems for code generation tools.
To model polymorphism, the discriminator keyword on a parent schema designates a property that determines the child schema used for validation. The property used as discriminator (i.e. value of propertyName) SHOULD be specified by the parent schema and listed as required. Objects with discriminator values that do not resolve to a child schema, are rejected during validation.
Rule: Discriminator mapping [sch-discr]
The URI-reference syntax (e.g. "#/components/schemas/Child") SHOULD be used to provide an explicit mapping of discriminator values to schemas.
Using schema names in discriminator mapping instead of URI-references is discouraged because it leads to implementation-specific behavior (as described in these two sections of the OpenAPI specification).
Version 3.0.4 of the OpenAPI Specification added that "a discriminator MUST NOT change the validation outcome of the schema". However, most existing tooling currently do reject values that are not valid against their corresponding child schema or cannot be mapped to one.
At time of writing, it is not possible to define schemas that comply with this change in the OpenAPI Specification, while still allowing polymorphism using common tooling.
Therefore, it is currently recommended to consider the discriminator having an effect on validation.
|
allOf to add additional restrictions or metadata| description | example |
|---|---|
Overriding the description of a property |
|
Adding a pattern restriction on a string |
|
Making a property of another schema required |
|
BAD : properties should not be duplicated in subschemas |
|
allOf for polymorphismParent:
type: object
properties:
parentProperty:
type: string
type:
type: string
required: [type]
discriminator:
propertyName: type
mapping:
first: "#/components/schemas/FirstChild"
second: "#/components/schemas/SecondChild"
FirstChild:
allOf:
- $ref: "#/components/schemas/Parent"
- type: object
properties:
firstChildProperty:
type: string
SecondChild:
allOf:
- $ref: "#/components/schemas/Parent"
- type: object
properties:
secondChildProperty:
type: integer
| JSON value | validation result against Parent schema |
|---|---|
|
valid validated against the |
|
invalid
|
|
invalid No child schema resolved for discriminator value |
Rule: Using oneOf [sch-oneof]
The oneOf JSON Schema keyword expresses that a value must be valid against exactly one of a list of subschemas. A value that is valid for more than one subschema, is considered invalid against the combined oneOf schema.
However, many uses of oneOf result in incompatibility problems with code generation tooling however. Thereby, use of oneOf SHOULD be limited to either:
-
subschemas with only a
requiredconstraint, without atypeor other constraint -
subschemas with an
enumof the same simpletype(i.e. noarrayorobject) -
subschemas of
type: object, when adiscriminatoris also defined. UnlikeallOf-style polymorphism ([sch-allof]), properties SHOULD only be defined within the subschemas.
When using oneOf in combination with a discriminator, the property used as discriminator (i.e. value of propertyName) SHOULD be specified by each oneOf subschema, and listed as required. The schemas referenced by the discriminator’s mapping SHOULD be the same as the oneOf subschemas. Objects with discriminator values that do not resolve to a subschema, are rejected during validation.
Examples below were tested with openapi-generator’s Java and Spring code generation.
oneOf compatible with toolingThese examples generate correct code:
| schema | generated code |
|---|---|
|
a class with all properties combined, and without required property (non-null) constraints |
|
an enum class |
|
A
This requires setting |
Following example illustrates bad usage of oneOf, as its subschemas are not designed to be mutually exclusive:
Agent:
type: object
oneOf:
- $ref: "#/components/schemas/Citizen"
- $ref: "#/components/schemas/Professional"
Citizen:
type: object
properties:
ssin:
$ref: "./belgif/person/identifier/v1/person-identifier-v1.yaml#/components/schemas/Ssin"
required: [ssin]
Professional:
type: object
properties:
ssin:
$ref: "./belgif/person/identifier/v1/person-identifier-v1.yaml#/components/schemas/Ssin"
enterpriseNumber:
$ref: "./belgif/organization/identifier/v1/organization-identifier-v1.yaml#/components/schemas/EnterpriseNumber"
required: [ssin, enterpriseNumber]
Any valid Professional object is now invalid against the Agent schema, e.g.:
{
"ssin": "53480200724",
"enterpriseNumber": "0123456789"
}
Schemas allow unspecified additional properties by default, so this value is valid against Citizen regardless of the enterpriseNumber property.
Being valid for both subschemas, means that it is invalid against the combined oneOf schema. Alternatively, polymorphism with allOf and discriminator can be used, or a single Agent schema which specifies all properties directly.
Disallowing additional properties isn’t recommended, as it limits API evolvability.
These examples do not work as expected:
| problematic schema | generated code |
|---|---|
|
empty class |
|
empty class |
|
With option
With |
Tools
Following tools can be used to edit OpenAPI documents
| Name | Link | Description |
|---|---|---|
Swagger UI |
Browser application. Graphical and text view of OpenAPI documents. Does not support references to external files. |
|
Zalando’s Swagger plugin |
Open Source plugin for IntelliJ. Text-only editor. |
|
Stoplight Studio |
Commercial editor with a free version. Graphical and text view, both web based or as desktop application. Supports validation of API style guides (Spectral). |
|
42Crunch OpenAPI (Swagger) Editor for VS Code |
https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi |
Open Source plugin for Visual Studio Code. Text editor with SwaggerUI preview and multi-file support. |
42Crunch OpenAPI (Swagger) Editor for IntelliJ |
https://plugins.jetbrains.com/plugin/14837-openapi-swagger-editor |
Plugin for IntelliJ. Text editor with multi-file support. |
IntelliJ OpenAPI Specifications |
Plugin bundled with IntelliJ Ultimate (commercial). Text editor with SwaggerUI preview and multi-file support. |
Following tools can be used to generate server stubs and API client libraries from OpenAPI documents.
| Name | Link | Comments |
|---|---|---|
openapi-generator |
Started as fork of swagger-codegen. |
|
swagger-codegen |
Following tools allow to verify REST styleguide rules for an OpenAPI document:
| Name | Link | Comments |
|---|---|---|
belgif-rest-guide-validator |
Open Source Maven Plugin to validate OpenAPI document against rules in this guide |
|
Spectral |
Open Source tool (javascript-based) to enforce REST styleguide rules. Note: There currently is no implementation of this REST guide for Spectral. |