From ae74e3c142cab8179a61ce874491fea846fa624d Mon Sep 17 00:00:00 2001 From: rishabhknowss Date: Thu, 29 Oct 2026 00:55:20 +0530 Subject: [PATCH] fix(jsonschema): emit unicode-safe patterns from concerto regexes Concerto regexes have no `u` flag, so their source may contain identity escapes such as `\<` that are invalid in unicode mode, which JSON Schema validators such as ajv use to compile `pattern`. Drop those redundant escapes when the source is not valid in unicode mode. Also compile every definition in the JSON Schema verification test, since ajv only compiles definitions that are referenced. Closes #300 Co-Authored-By: Claude Opus 5.5 Signed-off-by: rishabhknowss --- .../fromcto/jsonschema/jsonschemavisitor.js | 52 ++++++++++++++++++- .../fromcto/jsonschema/jsonschemavisitor.js | 18 +++++++ test/verification/jsonschema.validate.test.js | 5 ++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js b/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js index 7c11e79a..da4bfe7e 100644 --- a/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js +++ b/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js @@ -19,6 +19,56 @@ const { ModelUtil } = require('@accordproject/concerto-core'); const { throwUnrecognizedType } = require('../../../common/util'); const RecursionDetectionVisitor = require('./recursionvisitor'); +// Characters that may be escaped with a backslash in a unicode-mode regex +// (`-` may only be escaped inside a character class) +const UNICODE_ESCAPABLE = '^$\\.*+?()[]{}|/'; + +/** + * Returns a regex source that is also valid in unicode mode, which JSON Schema + * validators such as ajv use to compile `pattern`. Concerto regexes have no `u` + * flag, so they may contain identity escapes such as `\<` that are rejected in + * unicode mode. Those escapes match the literal character either way, so the + * backslash is dropped. Sources that are already valid are returned unchanged. + * @param {string} source - the regex source + * @return {string} the unicode-safe regex source + * @private + */ +function toUnicodeSafePattern(source) { + try { + new RegExp(source, 'u'); + return source; + } catch (err) { + debug('Regex is not valid in unicode mode, removing identity escapes', source); + } + let result = ''; + let inClass = false; + for (let i = 0; i < source.length; i++) { + const char = source[i]; + const next = source[i + 1]; + if (char === '\\' && next !== undefined) { + if (/[A-Za-z0-9]/.test(next) || UNICODE_ESCAPABLE.includes(next) || (inClass && next === '-')) { + result += char + next; + } else { + result += next; + } + i++; + } else { + if (char === '[') { + inClass = true; + } else if (char === ']') { + inClass = false; + } + result += char; + } + } + try { + new RegExp(result, 'u'); + return result; + } catch (err) { + return source; + } +} + /** * Convert the contents of a {@link ModelManager} to a JSON Schema, returning * the schema for all types under the 'definitions' key. If the 'rootType' @@ -89,7 +139,7 @@ class JSONSchemaVisitor { } else if(validator) { // validator for uuid is not required. // Note that regex flags are lost in this transformation if (validator.getRegex()) { - jsonSchema.pattern = validator.getRegex().source; + jsonSchema.pattern = toUnicodeSafePattern(validator.getRegex().source); } if(validator.getMinLength()) { jsonSchema.minLength = validator.getMinLength(); diff --git a/test/codegen/fromcto/jsonschema/jsonschemavisitor.js b/test/codegen/fromcto/jsonschema/jsonschemavisitor.js index 5ff763fa..0523559c 100644 --- a/test/codegen/fromcto/jsonschema/jsonschemavisitor.js +++ b/test/codegen/fromcto/jsonschema/jsonschemavisitor.js @@ -334,6 +334,24 @@ describe('JSONSchema (samples)', function () { expect(schema.properties.myString.pattern).equal('abc.*'); }); + it('should generate unicode-safe patterns from regexes with identity escapes', () => { + const modelManager = new ModelManager(); + modelManager.addCTOModel(`namespace test@1.0.0 + concept Test { + o String noAngleBrackets regex=/^[^\\<\\>]*$/ + o String escapedDash regex=/^a\\-b$/ + o String alreadySafe regex=/^[\\w\\-]+\\.\\d+$/ + }`); + const visitor = new JSONSchemaVisitor(); + const schema = modelManager.accept(visitor, { rootType: 'test@1.0.0.Test'}); + expect(schema.properties.noAngleBrackets.pattern).equal('^[^<>]*$'); + expect(schema.properties.escapedDash.pattern).equal('^a-b$'); + expect(schema.properties.alreadySafe.pattern).equal('^[\\w\\-]+\\.\\d+$'); + Object.values(schema.properties) + .filter(property => property.pattern) + .forEach(property => expect(() => new RegExp(property.pattern, 'u')).to.not.throw()); + }); + it('should inline types for simple model', () => { const modelManager = new ModelManager(); modelManager.addCTOModel( MODEL_SIMPLE ); diff --git a/test/verification/jsonschema.validate.test.js b/test/verification/jsonschema.validate.test.js index 032a6935..d54bb4cc 100644 --- a/test/verification/jsonschema.validate.test.js +++ b/test/verification/jsonschema.validate.test.js @@ -49,6 +49,11 @@ async function verifyJsonSchemaCompiles(modelManager, visitorOptions = {}) { const ajv = new Ajv({ strict: false }); try { ajv.compile(schema); + // ajv only compiles definitions that are referenced, so compile + // each definition through a $ref to check all of them + Object.keys(schema.definitions || {}).forEach(name => { + ajv.compile({ ...schema, $ref: `#/definitions/${name}` }); + }); } catch (err) { throw new Error(err.message); }