Skip to content

Instantly share code, notes, and snippets.

@handrews
Last active June 12, 2019 07:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save handrews/6dfebd56ef97328f9e4dc7a47a1e8bc7 to your computer and use it in GitHub Desktop.
Save handrews/6dfebd56ef97328f9e4dc7a47a1e8bc7 to your computer and use it in GitHub Desktop.
OAS 3.0 JSON Schema from PR #1270 rewritten with JSON Schema draft-08 proposals
$id: 'https://openapis.org/schemas/oas/3.0'
$schema: 'http://json-schema.org/draft-08-work-in-progress/schema#'
type: object
required:
- openapi
- info
- paths
allOf:
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
openapi:
type: string
pattern: ^3\.0\.\d(-.+)?$
info:
$ref: '#Info'
externalDocs:
$ref: '#ExternalDocumentation'
servers:
type: array
items:
$ref: '#Server'
security:
type: array
items:
$ref: '#SecurityRequirement'
tags:
type: array
items:
$ref: '#Tag'
paths:
$ref: '#Paths'
components:
$ref: '#Components'
unevaluatedProperties: false
$defs:
Mixins:
$defs:
Describable:
properties:
description:
type: string
Requireable:
properties:
required:
type: boolean
default: false
Deprecatable:
properties:
deprecated:
type: boolean
default: false
DescReqDep:
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Requireable'
- $ref: '#/$defs/Mixins/$defs/Deprecatable'
Extensible:
patternProperties:
'^x-': true
WithSingleExample:
properties:
example: true
examples: false
WithExampleObjects:
properties:
example: false
examples:
type: object
additionalProperties:
$ref: '#Example'
WithExamples:
oneOf:
- $ref: '#/$defs/Mixins/$defs/WithSingleExample'
- $ref: '#/$defs/Mixins/$defs/WithExampleObjects'
WithSchema:
properties:
schema:
$ref: '#Schema'
content: false
WithSchemaAndExamples:
allOf:
- $ref: '#/$defs/Mixins/$defs/WithSchema'
- $ref: '#/$defs/Mixins/$defs/WithExamples'
WithContent:
properties:
schema: false
content:
type: object
additionalProperties:
$ref: '#MediaType'
minProperties: 1
maxProperties: 1
WithNameAndLocation:
required:
- name
- in
properties:
name:
type: string
in:
type: string
enum:
- path
- query
- header
- cookie
WithExplodeAndReserved:
properties:
explode:
type: boolean
allowReserved:
type: boolean
default: false
WithEmpty:
properties:
allowEmptyValue:
type: boolean
default: false
WithExplodeReservedAndEmpty:
allOf:
- $refs: '#/$defs/Mixins/$defs/WithExplodeAndReserved'
- $refs: '#/$defs/Mixins/$defs/WithEmpty'
StyledSimple:
allOf:
- $refs: '#/$defs/Mixins/$defs/WithExplodeReservedAndEmpty'
properties:
style:
type: string
const: simple
default: simple
StyledMatrix:
properties:
style:
type: string
enum:
- matrix
- label
- simple
default: simple
StyledFormOnly:
allOf:
- $refs: '#/$defs/Mixins/$defs/WithExplodeReservedAndEmpty'
properties:
style:
type: string
const: form
default: form
StyledFormComplexNoDefaultOrEmpty:
allOf:
- $refs: '#/$defs/Mixins/$defs/WithExplodeAndReserved'
properties:
style:
type: string
enum:
- form
- spaceDelimited
- pipeDelimited
- deepObject
StyledFormComplex:
allOf:
- $refs: '#/$defs/Mixins/$defs/StyledFormComplexNoDefaultOrEmpty'
- $refs: '#/$defs/Mixins/$defs/WithExplodeReservedAndEmpty'
properties:
style:
default: form
Objects:
$defs:
Reference:
$id: '#Reference'
type: object
required:
- $ref
properties:
$ref:
type: string
format: uri-reference
Info:
type: object
required:
- title
- version
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
title:
type: string
termsOfService:
type: string
format: uri-reference
contact:
$ref: '#Contact'
license:
$ref: '#License'
$defs:
Contact:
$id: '#Contact'
type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
name:
type: string
url:
type: string
format: uri-reference
email:
type: string
format: email
unevaluatedProperties: false
License:
$id: '#License'
type: object
required:
- name
allOf:
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
name:
type: string
url:
type: string
format: uri-reference
unevaluatedProperties: false
Server:
$id: '#Server'
type: object
required:
- url
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
url:
type: string
variables:
type: object
additionalProperties:
type: object
required:
- default
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
enum:
type: array
items:
type: string
default:
type: string
unevaluatedProperties: false
unevaluatedProperties: false
Response:
$id: '#Response'
oneOf:
- $ref: '#Reference'
- type: object
required:
- description
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
headers:
type: object
additionalProperties:
$ref: '#Header'
content:
type: object
additionalProperties:
$ref: '#MediaType'
links:
type: object
additionalProperteis:
$ref: '#Link'
unevaluatedProperties: false
MediaType:
$id: '#MediaType'
type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/WithSchemaAndExamples'
properties:
encoding:
type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/StyledFormComplexNoDefaultOrEmpty'
properties:
contentType:
type: string
headers:
type: object
additionalProperties:
$ref: '#Header'
unevaluatedProperties: false
Example:
$id: '#Example'
oneOf:
- $ref: '#Reference'
- type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
summary:
type: string
value: true
externalValue:
type: string
format: uri-reference
unevaluatedProperties: false
Header:
$id: '#Header'
oneOf:
- $ref: '#Reference'
- type: object
required:
- schema
allOf:
- $ref: '#/$defs/Mixins/$defs/DescReqDep'
- $ref: '#/$defs/Mixins/$defs/Extensible'
oneOf:
- allOf:
- $ref: '#/$defs/Mixins/$defs/WithSchemaAndExamples'
- $ref: '#/$defs/Mixins/$defs/StyledSimple'
- allOf:
- $ref: '#/$defs/Mixins/$defs/WithContent'
- $ref: '#/$defs/Mixins/$defs/WithEmpty'
unevaluatedProperties: false
Paths:
$id: '#Paths'
type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/Extensible'
patternProperties:
'^\/':
type: object
patternProperties:
'^(get|put|post|delete|options|head|patch|trace)$':
$ref: '#Operation'
properties:
servers:
type: array
items:
$ref: 'Server'
parameters:
type: array
items:
$
unevaluatedProperties: false
Operation:
$id: '#Operation'
type: object
required:
- responses
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Deprecatable'
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
tags:
type: array
items:
type: string
summary:
type: string
externalDocs:
$ref: '#ExternalDocumentation'
operationId:
type: string
parameters:
type: array
items:
$ref: '#Parameter'
requestBody:
$ref: '#RequestBody'
responses:
$ref: '#Responses'
callback:
$ref: '#Callback'
security:
type: array
items:
$ref: '#SecurityRequirement'
servers:
type: array
items:
$ref: '#Server'
unevaluatedProperties: false
Responses:
$id: '#Responses'
type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
default:
$ref: '#Response'
patternProperties:
'[1-5](?:\d{2}|XX)':
$ref: '#Response'
minProperties: 1
unevaluatedProperties: false
SecurityRequirement:
$id: '#SecurityRequirement'
type: object
additionalProperties:
type: array
items:
type: string
Tag:
$id: '#Tag'
type: object
required:
- name
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
name:
type: string
externalDocs:
$ref: '#ExternalDocumentation'
unevaluatedProperties: false
Parameter:
$id: '#Parameter'
oneOf:
- $ref: '#Reference'
- type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/WithNameAndLocation'
- $ref: '#/$defs/Mixins/$defs/Extensible'
- oneOf:
- allOf:
- $ref: '#PathParam'
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Deprecatable'
- required:
- required
properties:
required:
const: true
default: true
- allOf:
- anyOf:
- $ref: '#QueryParam'
- $ref: '#HeaderParam'
- $ref: '#CookieParam'
- $ref: '#/$defs/Mixins/$defs/DescReqDep'
oneOf:
- allOf:
- required:
- schema
- $ref: '#/$defs/Mixins/$defs/WithSchemaAndExamples'
- oneOf:
- allOf:
- $ref: '#PathParam'
- $ref: '#/$defs/Mixins/$defs/StyledMatrix'
- allOf:
- $refs: '#QueryParam'
- $refs: '#/$defs/Mixins/$defs/StyledFormComplex'
- allOf:
- $ref: '#HeaderParam'
- $ref: '#/$defs/Mixins/$defs/StyledSimple'
- allOf:
- $ref: '#CookieParam'
- $refs: '#/$defs/Mixins/$defs/StyledFormOnly'
- allOf:
- required:
- content
- $ref: '#/$defs/Mixins/$defs/WithContent'
- $ref: '#/$defs/Mixins/$defs/WithEmpty'
unevaluatedProperties: false
$defs:
PathParam:
$id: '#PathParam'
properties:
in:
const: path
QueryParam:
$id: '#QueryParam'
properties:
in:
const: query
HeaderParam:
$id: '#HeaderParam'
properties:
in:
const: header
CookieParam:
$id: '#CookieParam'
properties:
in:
const: cookie
RequestBody:
$id: '#RequestBody'
required:
- content
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Requireable'
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
content:
type: object
additionalProperties:
$ref: '#MediaType'
unevaluatedProperties: false
SecurityScheme:
$id: '#SecurityScheme'
oneOf:
- $ref: '#Reference'
- type: object
required:
-type
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
type:
string:
enum:
- apiKey
- http
- bearer
- oauth2
- openIdConnect
oneOf:
- $ref: '#APIKey'
- $ref: '#HTTP'
- $ref: '#OAuth2'
- $ref: '#OpenIdConnect'
unevaluatedProperties: false
$defs:
APIKey:
$id: '#APIKey'
properties:
type:
const: apiKey
allOf:
- $ref: '#/$defs/Mixins/$defs/WithNameAndLocation'
not:
$ref: '#PathParam'
HTTP:
$id: '#HTTP'
required:
- scheme
properties:
scheme:
type: string
oneOf:
- properties:
type:
const: http
- properties:
type:
const: bearer
bearerFormat:
type: string
OAuth2:
$id: '#OAuth2'
required:
- flows
properties:
type:
const: oauth2
flows:
$ref: '#OAuthFlows'
OpenIdConnect:
$id: '#OpenIdConnect'
required:
- openIdConnectUrl
properties:
type:
const: openIdConnect
openIdConnectUrl:
type: string
format: uri
OAuthFlows:
$id: '#OAuthFlows'
type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/Extensible'
properties:
implicit:
allOf:
- required:
- scopes
- $ref: '#CommonFlow'
- $ref: '#AuthorizationFlow'
- $ref: '#/$defs/Mixins/$defs/Extensible'
unevaluatedProperties: false
password:
allOf:
- $ref: '#CommonFlow'
- $ref: '#TokenFlow'
- $ref: '#/$defs/Mixins/$defs/Extensible'
unevaluatedProperties: false
clientCredentials:
allOf:
- $ref: '#CommonFlow'
- $ref: '#TokenFlow'
- $ref: '#/$defs/Mixins/$defs/Extensible'
unevaluatedProperties: false
authorizationCode:
allOf:
- $ref: '#CommonFlow'
- $ref: '#TokenFlow'
- $ref: '#AuthorizationFlow'
- $ref: '#/$defs/Mixins/$defs/Extensible'
unevaluatedProperties: false
unevaluatedProperties: false
$defs:
Common:
$id: '#CommonFlow'
properties:
refreshUrl:
type: string
format: uri-reference
scopes:
type: object
additionalProperties:
type: string
Token:
$id: '#TokenFlow'
required:
- tokenUrl
properties:
tokenUrl:
type: string
format: uri-reference
Authorization:
$id: '#AuthorizationFlow'
required:
- authorizationUrl
properties:
authorizationUrl:
type: string
format: uri-reference
Link:
$id: '#Link'
oneOf:
- $ref: '#Reference'
- type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/Describable'
- $ref: '#/$defs/Mixins/$defs/Extensible'
oneOf:
- properties:
operationRef:
type: string
format: uri-reference
operationId: false
- properties:
operationRef: false
operationId:
type: string
properties:
parameters:
type: object
additionalProperties: true
requestBody: true
server:
$ref: '#Server'
unevaluatedProperties: false
Callback:
$id: '#Callback'
oneOf:
- $ref: '#Reference'
- type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/Extensible'
unevaluatedProperties: false
Schema:
$id: '#Schema'
oneOf:
- $ref: '#Reference'
- type: object
allOf:
- $ref: 'http://json-schema.org/draft-04/schema#'
- $ref: '#SchemaRestrictions'
- $ref: '#SchemaExtensions'
- $ref: '#/$defs/Mixins/$def/Extensible'
unevaluatedProperties: false
$defs:
Restrictions:
$id: '#SchemaRestrictions'
properties:
type:
not:
const: 'null'
not:
$ref: '#/$defs/Schema'
allOf:
type: array
items:
$ref: '#/$defs/Schema'
oneOf:
type: array
items:
$ref: '#/$defs/Schema'
anyOf:
type: array
items:
$ref: '#/$defs/Schema'
items:
$ref: '#/$defs/Schema'
properties:
type: object
additionalProperties:
$ref: '#/$defs/Schema'
additionalProperties:
oneOf:
- $ref: '#/$defs/Schema'
- type: boolean
default: true
patternProperties: false
Extensions:
$id: '#SchemaExtensions'
properties:
nullable:
type: boolean
default: false
discriminator:
$ref: '#Discriminator'
readOnly:
type: boolean
default: false
writeOnly:
type: boolean
default: false
example: true
externalDocs:
$ref: '#ExternalDocumentation'
deprecated:
type: boolean
default: false
xml:
$ref: '#XML'
Discriminator:
$id: '#Discriminator'
type: object
required:
- propertyName
properties:
propertyName:
type: string
mapping:
type: object
additionalProperties:
type: string
XML:
$id: '#XML'
type: object
properties:
name:
type: string
namespace:
type: string
format: url
prefix:
type: string
attribute:
type: boolean
default: false
wrapped:
type: boolean
default: false
patternProperties:
'^x-': true
additionalProperties: false
Components:
$id: '#Components'
type: object
allOf:
- $ref: '#/$defs/Mixins/$defs/Extensible'
- additionalProperties:
$comment: All sub-objects have the same property name constraints
type: object
propertyNames:
pattern: '^[a-zA-Z0-9\.\-_]+$'
properties:
schemas:
additionalProperties:
$ref: '#Schema'
responses:
additionalProperties:
$ref: '#Responses'
parameters:
additionalProperties:
$ref: '#Parameter'
examples:
additionalProperties:
$ref: '#Example'
requestBodies:
additionalProperties:
$ref: '#RequestBody'
headers:
additionalProperties:
$ref: '#Header'
links:
additionalProperties:
$ref: '#Link'
callbacks:
additionalProperties:
$ref: '#Callback'
unevaluatedProperties: true
@handrews
Copy link
Author

handrews commented Mar 2, 2018

This is a refactor of OAI/OpenAPI-Specification#1270 which uses draft-06 and draft-07 features, as well as several draft-08 proposals. It illustrates different things for the JSON Schema and OpenAPI projects.

  • For the JSON Schema project, it demonstrates the usefulness of draft-08 proposals, particularly unevaluatedProperties, in a complex real-world scenario.
  • For OpenAPI, it is more of a proof-of-concept and demonstration that recent drafts of JSON Schema offer real advantages over the draft that OAS mostly supports. In particular, unevaluatedProperties allows us to eliminate the extensive duplication in PR 1270.

High level overview

I broke the schema up into two kinds of definitions: mixins and objects. The mixins collect various individual or groups of properties that are re-used in many places. This includes some oneOf combinations which are either mutually exclusive due to a pinned enum value, or I made them mutually exclusive by forbidding each other's properties by setting the property schemas to false (see Example and Examples for... er... examples :-)

While organizing the mixins like this maximizes re-use, it does obscure the structure of things a bit. However, I don't think that schemas should be primary documentation- that's what the spec is for. The schema needs to be correct and maintainable more than it needs to be obvious for less techncial human readers.

Here is a quick guide to the post-draft-04 features used above:

  • draft-06

    • const in place of single-element enum
    • boolean schemas are allowed everywhere for clarity and optimization
      • true in place of {}
      • false in place of {"not": {}}
    • $id instead of id
      • I gave the object schemas $id plain name fragments
      • The mixins are all referenced by the JSON Pointer fragment
      • This is just to show both fragment / $ref styles, the choice is not otherwise significant here
    • propertyNames, a schema applied to all property names in the instance
      • This can be used to replace certain uses of patternProperties
      • See the Components object for an example of how that made it possible to reduce duplication
    • "format": "uri-reference" instead of uriref
  • draft-07

    • I did not end up using if/then/else, although some of the oneOfs could be reworked to use it
    • The OAS schema doesn't need contains or any of the newer formats
  • draft-08 proposals

    • rename definitions to $defs
      • It fits better with $ref, $id, and $schema as it has no validation effect
      • The keyword also moves over to the core spec
    • unevaluatedProperties gets its own section below

unevaluatedProperties

The unevaluatedProperties proposal is key, so let's go through an example of how it works.

TL;DR: It does what you probably want additionalProperties to do when you put it next to anyOf, allOf, or oneOf.

Both of the following schemas successfully validate {"foo": 1, "common": true} and {"bar": "hi", "common": false}, but fail on{"foo": 1, "common": true, "oof" 2} for different reasons in each schema. Let's take a look at what those different reasons are:

type: object
anyOf:
  - properties:
      common: {type: boolean}
      foo: {type: integer}
    additionalProperties: false
  - properties:
      common: {type: boolean}
      bar: {type: string}
    additionalProperties: false

In the above schema, {"foo": 1, "common": true, "oof: 2} fails because of the additionalProperties: false adjacent to the definition of "foo". It prevents "off", and any other property but "foo" or "common", from validating.

However, note that common: {type: boolean} is duplicated in the two branches of the anyOf. This is because additionalProperties only "sees" properties and patternProperties that are defined immediately adjacent to it in the same schema object. This is also why additionalProperties: false is duplicated. If additionalProperties were outside of the anyOf, it would not have any immediately adjacent properties or patternProperties so any object with any properties would fail validation.

One benefit of the way additionalProperties is defined is that you can compute the set of property names to which it applies statically from the schema alone, without evaluating an instance.

Now let's look at it with unevaluatedProperties:

type: object
properties:
  common: {type: boolean}
anyOf:
  - properties:
      foo: {type: integer}
  - properties:
      bar: {type: string}
unevaluatedProperties: false

Here we see common defined once at the top level, alongside a top level unevaluatedProperties: false which replaces the two separate uses of additionalProperties: false.

unevaluatedProperties can "see" into the anyOf, because it applies to any property that has not successfully validated against any subschema of any keyword adjacent to it.

In this example, the anyOf and the outer properties are both adjacent to unevaluatedProperties. When validating {"foo": 1, "common": true}, "common" is successfully evaluated against a subschema of the adjacent properties, and "foo" is successfully evaluated against a subschema under the anyOf.

However, when validating

{"foo": 1, "common": true, "oof": 2}

"oof" has not been validated against any defined subschema, so it is considered to be unevaluated. And since we have set unevaluatedProperties to false, its presence causes validation to fail.

Considering a more complex anyOf:

type: object
properties:
  common: {type: boolean}
anyOf:
  - properties:
      foo: {type: integer}
  - required: [bar]
    properties:
      bar: {type: string}
      oof: {type: integer}
unevaluatedProperties: false

With this schema, {"bar": "hi", "common": false, "oof": 2} is valid, while {"foo": 1, "common": true, "oof": 2} is not. This shows how unevaluatedProperties is dynamic in nature.

When the first instance matches the subschema with "bar", it causes "bar" and "oof" to be evaluated. And "common" is still evaluated by the top-level properties. Therefore there are no unevaluated properties in the instance and validation passes.

The second instance fails the "bar" branch of the anyOf. This means that "oof" is not considered to have been evaluated. Even though it passes the "oof" subschema in the "bar" branch, since the whole "bar" branch fails validation (because there is no "bar" in the instance), no subschema in the branch contributes to the evaluation count.

The second instance only passes the "foo" branch, so only "foo" and "common" are considered to have been evaluated. "oof" has not been, so unevaluatedProperties: false causes it to fail.

@dskvr
Copy link

dskvr commented Jun 12, 2019

What is the likelihood of unevaluatedProperties being accepted as the json-schema for OAS 3.0? OAI/OpenAPI-Specification#1032 was accepted last month and does not include unevaluatedProperties.

@handrews
Copy link
Author

@dskvr unevaluatedProperties is part of the next draft of JSON Schema, so #1032 couldn't make use of it yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment