From 20f10b41fed58604dafe55c29f8ab2cf875b4c7d Mon Sep 17 00:00:00 2001 From: Howard Askew <19271569+howaskew@users.noreply.github.com> Date: Wed, 29 Jul 2026 11:58:15 +0100 Subject: [PATCH] fix: don't throw when @context contains a non-string entry OpenactiveUrlsCorrectRule assumed every entry in an `@context` array was a string and called `.match()` on each one. JSON-LD permits an `@context` array to combine IRIs with inline context definitions (maps), so a conformant-JSON-LD-but-non-conformant-OpenActive feed such as: "@context": ["https://openactive.io/", {"schema": "https://schema.org/"}] caused `TypeError: context.match is not a function`. In data-model-validator-site this surfaces to the publisher as an opaque "Internal validator error" with no indication of what is wrong, and no other errors in the document are reported. The validator already handles this case correctly: ContextInRootNodeRule raises its `type` failure ("Whilst JSON-LD supports inline context objects, for use in OpenActive the @context property must contain a URL or array of URLs...") for exactly this input. The crash happened before that error could be surfaced. Non-string entries are now filtered out before the URL check, so the rule inspects only the entries it is able to inspect. Behaviour is otherwise unchanged: incorrect OpenActive URLs are still flagged, including when they appear alongside a non-string entry. This also fixes the same crash for `null`, numeric and nested-array entries. Tests: unit coverage for non-string entries (object, null, number, nested array) in the rule spec, plus an end-to-end regression test in validate-spec asserting that ContextInRootNodeRule reports the failure rather than the run throwing. --- .../openactive-urls-correct-rule-spec.js | 74 +++++++++++++++++++ .../openactive-urls-correct-rule.js | 8 +- src/validate-spec.js | 22 ++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/rules/data-quality/openactive-urls-correct-rule-spec.js b/src/rules/data-quality/openactive-urls-correct-rule-spec.js index ef9e6252..7f75634e 100644 --- a/src/rules/data-quality/openactive-urls-correct-rule-spec.js +++ b/src/rules/data-quality/openactive-urls-correct-rule-spec.js @@ -76,6 +76,80 @@ describe('OpenactiveUrlsCorrectRule', () => { } }); + it('should return no errors if the context array contains non-string entries', async () => { + // JSON-LD permits an @context array to combine IRIs with inline context + // definitions. Those entries are not URLs, so this rule has nothing to + // check and must not throw. ContextInRootNodeRule reports them instead. + const dataItems = [ + { + '@context': [metaData.contextUrl, { schema: 'https://schema.org/' }], + '@type': 'Event', + }, + { + '@context': [{ schema: 'https://schema.org/' }], + '@type': 'Event', + }, + { + '@context': [metaData.contextUrl, null], + '@type': 'Event', + }, + { + '@context': [metaData.contextUrl, 42], + '@type': 'Event', + }, + { + '@context': [metaData.contextUrl, ['https://schema.org/']], + '@type': 'Event', + }, + ]; + + for (const data of dataItems) { + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(0); + } + }); + + it('should still return an error for an incorrect OpenActive URL alongside a non-string entry', async () => { + const dataItems = [ + { + '@context': [{ schema: 'https://schema.org/' }, 'http://openactive.io/'], + '@type': 'Event', + }, + { + '@context': ['http://www.openactive.io/', { schema: 'https://schema.org/' }], + '@type': 'Event', + }, + { + '@context': [null, 'https://www.openactive.io/'], + '@type': 'Event', + }, + ]; + + for (const data of dataItems) { + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + + for (const error of errors) { + expect(error.type).toBe(ValidationErrorType.INVALID_FORMAT); + expect(error.severity).toBe(ValidationErrorSeverity.FAILURE); + } + } + }); + it('should return an error if the context is present, but contains a field not matching the correct scheme / domain', async () => { const dataItems = [ { diff --git a/src/rules/data-quality/openactive-urls-correct-rule.js b/src/rules/data-quality/openactive-urls-correct-rule.js index 8af047ad..6159921d 100644 --- a/src/rules/data-quality/openactive-urls-correct-rule.js +++ b/src/rules/data-quality/openactive-urls-correct-rule.js @@ -34,7 +34,13 @@ module.exports = class OpenactiveUrlsCorrectRule extends Rule { if (typeof fieldValue === 'string') { contexts.push(fieldValue); } else if (fieldValue instanceof Array) { - contexts = fieldValue.slice(); + // A JSON-LD @context array may legitimately combine IRIs with inline + // context definitions (maps). This rule can only inspect IRIs, so + // non-string entries are filtered out here. They are reported + // separately by ContextInRootNodeRule, which raises a failure for any + // @context that is not a URL or an array of URLs. Filtering keeps that + // error reachable rather than throwing before it can be raised. + contexts = fieldValue.filter((context) => typeof context === 'string'); } for (const context of contexts) { diff --git a/src/validate-spec.js b/src/validate-spec.js index 99d85bb3..d4fe6317 100644 --- a/src/validate-spec.js +++ b/src/validate-spec.js @@ -619,6 +619,28 @@ describe('validate', () => { expect(typeof result).toBe('object'); }); + it('should not throw if @context contains an inline context object', async () => { + // Regression test: an @context array combining an IRI with an inline + // context definition is valid JSON-LD but not permitted by OpenActive. + // It must be reported as a conformance failure, not crash the run. + const data = { + '@context': [metaData.contextUrl, { schema: 'https://schema.org/' }], + '@type': 'Event', + name: 'Tai chi Class', + }; + + const result = await validate(data, options); + + const contextErrors = result.filter( + (error) => error.rule === 'ContextInRootNodeRule', + ); + + expect(contextErrors.length).toBe(1); + expect(contextErrors[0].type).toBe(ValidationErrorType.INVALID_TYPE); + expect(contextErrors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + expect(contextErrors[0].path).toBe('$["@context"]'); + }); + it('should return an unsupported warning if nested arrays are passed', async () => { const event = { ...validSessionSeries };