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
71 changes: 65 additions & 6 deletions packages/http/src/validator/validators/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,19 @@ describe('convertAjvErrors()', () => {
it('converts properly', () => {
expect(
convertAjvErrors(
[Object.assign({}, errorObjectFixture, {
params: { unevaluatedProperty: 'd' },
keyword: 'unevaluatedProperties',
message: 'must NOT have unevaluated propertes',
})],
[
Object.assign({}, errorObjectFixture, {
params: { unevaluatedProperty: 'd' },
keyword: 'unevaluatedProperties',
message: 'must NOT have unevaluated propertes',
}),
],
DiagnosticSeverity.Error,
ValidationContext.Input
)[0]
).toHaveProperty('message', "Request parameter a.b must NOT have unevaluated propertes: 'd'");
});
});
});
});

describe('validateAgainstSchema()', () => {
Expand Down Expand Up @@ -224,6 +226,63 @@ describe('validateAgainstSchema()', () => {
});
});

describe('uses OpenAPI 3.1 JSON Schema dialect defaults', () => {
const schemaWithUnevaluatedProperties = {
type: 'object',
properties: {
name: { type: 'string' },
},
unevaluatedProperties: false,
} as JSONSchema7;

it('uses the document-level jsonSchemaDialect when a schema has no $schema URI', () => {
assertSome(
validateAgainstSchema(
{ name: 'Ada', extra: true },
schemaWithUnevaluatedProperties,
false,
ValidationContext.Output,
'body',
{ openapi: '3.1.0', jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema' }
),
error =>
expect(error).toContainEqual(
expect.objectContaining({
code: 'unevaluatedProperties',
message: "Response body must NOT have unevaluated properties: 'extra'",
path: ['body'],
})
)
);
});

it('defaults OpenAPI 3.1 documents to JSON Schema 2020-12', () => {
assertSome(
validateAgainstSchema(
{ name: 'Ada', extra: true },
schemaWithUnevaluatedProperties,
false,
ValidationContext.Output,
'body',
{ openapi: '3.1.0' }
),
error => expect(error).toContainEqual(expect.objectContaining({ code: 'unevaluatedProperties' }))
);
});

it('keeps using draft-07 when neither $schema nor OpenAPI 3.1 dialect information is available', () => {
assertNone(
validateAgainstSchema(
{ name: 'Ada', extra: true },
schemaWithUnevaluatedProperties,
false,
ValidationContext.Output,
'body'
)
);
});
});

describe('with coercing', () => {
it('will not return error for convertible values', () => {
assertNone(
Expand Down
42 changes: 33 additions & 9 deletions packages/http/src/validator/validators/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,42 @@ const ajvInstances = {

const JSON_SCHEMA_DRAFT_2019_09 = /^https?:\/\/json-schema.org\/draft\/2019-09\/schema#?$/;
const JSON_SCHEMA_DRAFT_2020_12 = /^https?:\/\/json-schema.org\/draft\/2020-12\/schema#?$/;
const OAS_3_1 = /^3\.1(?:\.|$)/;

function assignAjvInstance($schema: string, coerce: boolean): AjvCore {
const member = coerce ? 'coerce' : 'noCoerce';
let draft: keyof typeof ajvInstances = 'default';
type JsonSchemaDraft = keyof typeof ajvInstances;

function selectJsonSchemaDraft($schema?: string, bundle?: unknown): JsonSchemaDraft {
if ($schema) {
if (JSON_SCHEMA_DRAFT_2019_09.test($schema)) {
return 'draft2019_09';
}

if (JSON_SCHEMA_DRAFT_2020_12.test($schema)) {
return 'draft2020_12';
}

if (JSON_SCHEMA_DRAFT_2019_09.test($schema)) {
draft = 'draft2019_09';
} else if (JSON_SCHEMA_DRAFT_2020_12.test($schema)) {
draft = 'draft2020_12';
return 'default';
}

return ajvInstances[draft][member];
if (typeof bundle === 'object' && bundle !== null) {
const { jsonSchemaDialect, openapi } = bundle as { jsonSchemaDialect?: unknown; openapi?: unknown };

if (typeof jsonSchemaDialect === 'string') {
return selectJsonSchemaDraft(jsonSchemaDialect);
}

if (typeof openapi === 'string' && OAS_3_1.test(openapi)) {
return 'draft2020_12';
}
}

return 'default';
}

function assignAjvInstance($schema: string | undefined, coerce: boolean, bundle?: unknown): AjvCore {
const member = coerce ? 'coerce' : 'noCoerce';

return ajvInstances[selectJsonSchemaDraft($schema, bundle)][member];
}

export const convertAjvErrors = (
Expand Down Expand Up @@ -139,7 +163,7 @@ export const validateAgainstSchema = (
bundle?: unknown
): O.Option<NonEmptyArray<IPrismDiagnostic>> =>
pipe(
O.tryCatch(() => getValidationFunction(assignAjvInstance(String(schema.$schema), coerce), schema, bundle)),
O.tryCatch(() => getValidationFunction(assignAjvInstance(schema.$schema, coerce, bundle), schema, bundle)),
O.chainFirst(validateFn => O.tryCatch(() => validateFn(value))),
O.chain(validateFn => pipe(O.fromNullable(validateFn.errors), O.chain(fromArray))),
O.map(errors => convertAjvErrors(errors, DiagnosticSeverity.Error, context, prefix))
Expand Down