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
5 changes: 5 additions & 0 deletions .changeset/863-nullable-message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@asyncapi/parser': patch
---

Guard nullable `message` fields in v2 ruleset selectors to prevent validation crashes (issue #863).
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export function asyncApi2MessageExamplesParserRule(parser: Parser): RuleDefiniti
recommended: true,
given: [
// messages
'$.channels.*.[publish,subscribe][?(@property === \'message\' && @.schemaFormat !== void 0)]',
'$.channels.*.[publish,subscribe][?(@property === \'message\' && !@null && @.schemaFormat !== void 0)]',
'$.channels.*.[publish,subscribe].message.oneOf[?(!@null && @.schemaFormat !== void 0)]',
'$.components.channels.*.[publish,subscribe].message[?(@property === \'message\' && @.schemaFormat !== void 0)]',
'$.components.channels.*.[publish,subscribe].message[?(@property === \'message\' && !@null && @.schemaFormat !== void 0)]',
'$.components.channels.*.[publish,subscribe].message.oneOf[?(!@null && @.schemaFormat !== void 0)]',
'$.components.messages[?(!@null && @.schemaFormat !== void 0)]',
// message traits
Expand Down
16 changes: 8 additions & 8 deletions packages/parser/src/ruleset/v2/ruleset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ export const v2CoreRuleset = {
recommended: true,
given: [
// messages
'$.channels.*.[publish,subscribe][?(@property === \'message\' && @.schemaFormat === void 0)]',
'$.channels.*.[publish,subscribe][?(@property === \'message\' && !@null && @.schemaFormat === void 0)]',
'$.channels.*.[publish,subscribe].message.oneOf[?(!@null && @.schemaFormat === void 0)]',
'$.components.channels.*.[publish,subscribe][?(@property === \'message\' && @.schemaFormat === void 0)]',
'$.components.channels.*.[publish,subscribe][?(@property === \'message\' && !@null && @.schemaFormat === void 0)]',
'$.components.channels.*.[publish,subscribe].message.oneOf[?(!@null && @.schemaFormat === void 0)]',
'$.components.messages[?(!@null && @.schemaFormat === void 0)]',
// message traits
Expand Down Expand Up @@ -201,9 +201,9 @@ export const v2SchemasRuleset = (parser: Parser) => {
severity: 'error',
recommended: true,
given: [
'$.channels[*][publish,subscribe][?(@property === \'message\' && @.schemaFormat === void 0)].payload.default^',
'$.channels[*][publish,subscribe][?(@property === \'message\' && !@null && @.schemaFormat === void 0)].payload.default^',
'$.channels.*.parameters.*.schema.default^',
'$.components.channels[*][publish,subscribe][?(@property === \'message\' && @.schemaFormat === void 0)].payload.default^',
'$.components.channels[*][publish,subscribe][?(@property === \'message\' && !@null && @.schemaFormat === void 0)].payload.default^',
'$.components.channels.*.parameters.*.schema.default^',
'$.components.schemas.*.default^',
'$.components.parameters.*.schema.default^',
Expand All @@ -223,9 +223,9 @@ export const v2SchemasRuleset = (parser: Parser) => {
severity: 'error',
recommended: true,
given: [
'$.channels[*][publish,subscribe][?(@property === \'message\' && @.schemaFormat === void 0)].payload.examples^',
'$.channels[*][publish,subscribe][?(@property === \'message\' && !@null && @.schemaFormat === void 0)].payload.examples^',
'$.channels.*.parameters.*.schema.examples^',
'$.components.channels[*][publish,subscribe][?(@property === \'message\' && @.schemaFormat === void 0)].payload.examples^',
'$.components.channels[*][publish,subscribe][?(@property === \'message\' && !@null && @.schemaFormat === void 0)].payload.examples^',
'$.components.channels.*.parameters.*.schema.examples^',
'$.components.schemas.*.examples^',
'$.components.parameters.*.schema.examples^',
Expand Down Expand Up @@ -338,9 +338,9 @@ export const v2RecommendedRuleset = {
recommended: true,
formats: AsyncAPIFormats.filterByMajorVersions(['2']).excludeByVersions(['2.0.0', '2.1.0', '2.2.0', '2.3.0']).formats(), // message.messageId is available starting from v2.4.
given: [
'$.channels.*.[publish,subscribe][?(@property === "message" && @.oneOf == void 0)]',
'$.channels.*.[publish,subscribe][?(@property === "message" && !@null && @.oneOf == void 0)]',
'$.channels.*.[publish,subscribe].message.oneOf.*',
'$.components.channels.*.[publish,subscribe][?(@property === "message" && @.oneOf == void 0)]',
'$.components.channels.*.[publish,subscribe][?(@property === "message" && !@null && @.oneOf == void 0)]',
'$.components.channels.*.[publish,subscribe].message.oneOf.*',
'$.components.messages.*',
],
Expand Down
66 changes: 66 additions & 0 deletions packages/parser/test/nullable-message-863.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Parser } from '../src/parser';

/**
* Regression test for issue #863: a payload property literally named `message`
* with a null example value crashed document validation.
*
* The v2 ruleset selected messages with JSONPath expressions of the form
*
* $.channels.*.[publish,subscribe][?(@property === 'message' && @.schemaFormat === void 0)]
*
* `@property === 'message'` also matches a *schema property* named `message`,
* not just a message object. When that property's example value is `null`,
* `@.schemaFormat` dereferences null and Spectral aborts the whole validation:
*
* jsonPath: Cannot read properties of null (reading 'schemaFormat')
*
* Adding `!@null` to those expressions skips null values before the member
* access. This test drives the public `parser.validate()` path with the exact
* document shape from the issue, so it fails on master with the thrown error and
* passes once the guards are in place.
*/
describe('issue #863: payload property named "message" with a null example', function () {
const parser = new Parser();

const documentRaw = {
asyncapi: '2.6.0',
info: { title: 'Nullable message property', version: '1.0.0' },
channels: {
'user/signup': {
publish: {
message: {
payload: {
type: 'object',
properties: {
// A schema property that happens to be named `message`.
message: { type: ['string', 'null'], maxLength: 50 },
},
required: ['message'],
},
examples: [{ name: 'return', payload: { message: null } }],
},
},
},
},
};

it('validates without throwing on the null example value', async function () {
// The bug surfaced as a thrown Error, not as a diagnostic, so the assertion
// is that validate() resolves at all. Asserting only "no diagnostics" would
// not catch it: on master this rejects before diagnostics are produced.
await expect(parser.validate(documentRaw)).resolves.toBeDefined();
});

it('does not report the jsonPath null-dereference as a diagnostic either', async function () {
const diagnostics = await parser.validate(documentRaw);
const nullDeref = diagnostics.filter((d: { message: string }) =>
/Cannot read properties of null/.test(d.message),
);
expect(nullDeref).toEqual([]);
});

it('still parses the document into a model', async function () {
const { document } = await parser.parse(documentRaw);
expect(document).toBeDefined();
});
});
Loading