Modifying schema validations options in Openapi-request-validator

Sigal Shaharabani
Israeli Tech Radar
Published in
4 min readJan 23, 2023

From https://www.openapis.org/about:

The OpenAPI Initiative (OAI) was created by a consortium of forward-looking industry experts who recognize the immense value of standardizing on how APIs are described. As an open governance structure under the Linux Foundation, the OAI is focused on creating, evolving and promoting a vendor neutral description format. The OpenAPI Specification was originally based on the Swagger Specification, donated by SmartBear Software.

I recently started working on a new Express project written in Typescript. A mono-repo with many API endpoints, to describe our endpoints we use OpenAPI Yaml files, we also created a middleware that validates the request body according to the OpenAPI specification and returns errors accordingly in case of failure. In this post, I will show how you can, like we did, modify the schema validation options.

Overwhelmed
Photo by Dmitry Ratushny on Unsplash

Our story begins with a oneof:

oneof — validates the value against exactly one of the subschemas

We had a Yaml block:

oneOf:
- $ref: '#/components/schemas/FooRequest'
- $ref: '#/components/schemas/BarRequest'
- $ref: '#/components/schemas/MeRequest'
- $ref: '#/components/schemas/YouRequest'
discriminator:
propertyName: my_type
mapping:
foo: '#/components/schemas/FooRequest'
bar: '#/components/schemas/BarRequest'
me: '#/components/schemas/MeRequest'
you: '#/components/schemas/YouRequest'

However, in case of a bad request (missing or illegal fields), we received errors on almost every field in the union of oneof.

Which brought us to: Why are the discriminator and mapping sections ignored?

The package we’re using for the validation is openapi-request-validator, and it is based on the specification created by openapi-framework.

const framework = new OpenAPIFramework({
apiDoc: openApiSpec,
operations: {},
name: openApiSpec.info.title,
featureType: 'default',
logger: dummyLogger, // to avoid unnecessary warnings
});
await framework.initialize({...});

...
requestValidator.validateRequest({
body: req.body,
headers: req.headers,
params: req.parameters,
query: req.query,
});

So, I dove in trying to understand how “oneof” is handled. The validators are based on a package called AJV — which is aimed to validate JSon schemas (I assume the Yaml schema is transformed into JSon and the JSon is validated). It all boils down to the validator class constructor: OpenAPIRequestValidator

const v = new Ajv({
useDefaults: true,
allErrors: true,
strict: false,
// @ts-ignore TODO get Ajv updated to account for logger
logger: false,
...(args.ajvOptions || {}),
});

AJV actually does support discriminator according to the documentation, but it is nullable and null is regarded as false by AJV. But we made some progress, if we can modify args.ajvOptions we can enable “discriminator”.

args are the request validator constructur’s arguments:

constructor(args: OpenAPIRequestValidatorArgs) { ... }

Let us see how a request validator is created (from openapi-framework):

const RequestValidatorClass =
this.features?.requestValidator || OpenAPIRequestValidator;

const requestValidator = new RequestValidatorClass({
errorTransformer: this.errorTransformer,
logger: this.logger,
parameters: methodParameters,
schemas: this.apiDoc.definitions, // v2
componentSchemas: this.apiDoc.components // v3
? this.apiDoc.components.schemas
: undefined,
externalSchemas: this.externalSchemas,
customFormats: this.customFormats,
customKeywords: this.customKeywords,
requestBody,
});

The constructor doesn’t receive any extra parameters, but notice: this.features?.requestValidator. Looks like we can inject a new request validator type, that is an option that was added to the library in January 2022.

And so we did:

Step 1: Created a new validator class with discriminator turned on:

export class UseDiscriminatorRequestValidator extends OpenAPIRequestValidator {
constructor(args: OpenAPIRequestValidatorArgs) {
args.ajvOptions = {
...args.ajvOptions,
discriminator: true,
};
super(args);
}
}

Step 2: Specify the type when creating the framework:

const framework = new OpenAPIFramework({
apiDoc: openApiSpec,
operations: {},
name: openApiSpec.info.title,
featureType: 'default',
logger: dummyLogger, // to avoid unnecessary warnings
features: {
requestValidator: UseDiscriminatorRequestValidator,
},
});

And voilà!

Not exactly

Despair
Photo by frank mckenna on Unsplash

Typescript compilation

The Typescript compilation doesn’t recognize “discriminator”. Some dependecy resolution mixup: Openapi-request-validator depends on AJV 8 which does support discriminator, but the package.lock.json enforced AJV 6 which does not.

I fixed the lock file and the code finally compiled.

Runtime errors

I ran the Express application and it crashed with an error from AJV: “mapping” is not supported.

To remind you of our YAML:

oneOf:
- $ref: '#/components/schemas/FooRequest'
- $ref: '#/components/schemas/BarRequest'
- $ref: '#/components/schemas/MeRequest'
- $ref: '#/components/schemas/YouRequest'
discriminator:
propertyName: my_type
mapping:
foo: '#/components/schemas/FooRequest'
bar: '#/components/schemas/BarRequest'
me: '#/components/schemas/MeRequest'
you: '#/components/schemas/YouRequest'

Mapping helps the discriminator decide which schema to use, but AJV declares that mapping is not supported and refuses to start if mapping is in the schema.

I decided to remove the mapping and hope for the best.

oneOf:
- $ref: '#/components/schemas/FooRequest'
- $ref: '#/components/schemas/BarRequest'
- $ref: '#/components/schemas/MeRequest'
- $ref: '#/components/schemas/YouRequest'
discriminator:
propertyName: my_type

I ran the Express application again and it crashed with an error: “my_type” is not a property in the subschemas.

The subschemas we used looked similar to this:

FooRequest:
x-internal: true
type: object
allOf:
- $ref: '#/components/schemas/BaseRequest'
- type: object
properties:
foo:
type: string
my_type:
type: string
enum:
- type1
required:
- my_type

Apparently, and it is documented in the RFC (page 15), the discriminator field must be a property and not in one of the subschemas. I modified the schema to have the discriminator field a property as below:

FooRequest:
x-internal: true
type: object
properties:
my_type:
type: string
enum:
- type1
allOf:
- $ref: '#/components/schemas/BaseRequest'
- type: object
properties:
foo:
type: string
required:
- my_type

And finally the Express application started, and when I ran an HTTP request with an illegal body the discriminator validation was used and I received the correct error messages for the appropriate subschema. The issue is finally resolved.

I suspect that I received the errors for the correct subschema despite not having a mapping, because we use an enum for the discriminator field.

To sum up the solution:

  1. Made sure we’re using AJV 8
  2. Injected a request validator type which turns on discriminator validation
  3. Removed discriminator mapping from the schema
  4. Made sure the discriminator field is a property in all the “oneof” schemas

Retrospective

It could have been nice if https://github.com/kogosoftwarellc/open-api had documentation to read or even update with a PR, but since it doesn’t, which is okay, I hope this post can assist you in your future endeavors of OpenAPI validations.

--

--