From 6a1b3d8b3d8acd65867c162de6bad4d5ca76be1c Mon Sep 17 00:00:00 2001 From: Norbert Hartl Date: Sat, 1 Aug 2026 12:18:58 +0200 Subject: [PATCH 1/3] Support ECMA-262 non-capturing groups (?:...) in pattern/patternProperties Pharo's Regex engine raises 'Invalid lookaround expression ?:' on (?:...) non-capturing groups, which are common in real-world schemas (e.g. the official OAI OpenAPI 3.0 meta-schema's Responses pattern '^[1-5](?:\d{2}|XX)$') - any document validation that reached such a pattern crashed outright instead of returning valid/invalid. Added String>>asECMARegex, which rewrites (?: to ( before compiling. Semantically equivalent for the boolean match/no-match checks that pattern and patternProperties need; both call sites now use it. Co-Authored-By: Claude Sonnet 5 --- .../JSONSchemaTests.class.st | 23 +++++++++++++++++++ .../JSONSchemaPatternConstraint.class.st | 2 +- ...SchemaPatternPropertiesConstraint.class.st | 2 +- source/JSONSchema-Core/String.extension.st | 8 +++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st b/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st index 4c111c8..99287b2 100644 --- a/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st +++ b/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st @@ -25,6 +25,29 @@ JSONSchemaTests >> testMultipleConstraintsOnAnyObject [ should: [ schema validate: '1234567' ] raise: JSONConstraintError ] +{ #category : 'tests' } +JSONSchemaTests >> testPatternPropertiesWithNonCapturingGroupIsSupported [ + "Same regression as #testPatternWithNonCapturingGroupIsSupported, but for the + patternProperties keyword (matches property *names*, separate code path/call site)." + | schema | + schema := JSONSchema fromString: '{"type":"object","patternProperties":{"^[1-5](?:\d{2}|XX)$":{"type":"string"}},"additionalProperties":false}'. + self shouldnt: [ schema validate: {'200'->'ok'} asDictionary ] raise: Error. + self shouldnt: [ schema validate: {'2XX'->'ok'} asDictionary ] raise: Error. + self should: [ schema validate: {'999'->'ok'} asDictionary ] raise: JSONSchemaError +] + +{ #category : 'tests' } +JSONSchemaTests >> testPatternWithNonCapturingGroupIsSupported [ + "Regression: the OAI OpenAPI-3.0 meta-schema's Responses object uses the pattern + '^[1-5](?:\d{2}|XX)$' - Pharo's Regex engine used to raise 'Invalid lookaround + expression ?:' on the (?:...) syntax, crashing any validation that hit it." + | schema | + schema := JSONSchema fromString: '{"type":"string","pattern":"^[1-5](?:\d{2}|XX)$"}'. + self shouldnt: [ schema validate: '200' ] raise: Error. + self shouldnt: [ schema validate: '2XX' ] raise: Error. + self should: [ schema validate: '999' ] raise: JSONConstraintError +] + { #category : 'tests' } JSONSchemaTests >> testSchemaReadArray [ diff --git a/source/JSONSchema-Core/JSONSchemaPatternConstraint.class.st b/source/JSONSchema-Core/JSONSchemaPatternConstraint.class.st index d5c6016..c203b63 100644 --- a/source/JSONSchema-Core/JSONSchemaPatternConstraint.class.st +++ b/source/JSONSchema-Core/JSONSchemaPatternConstraint.class.st @@ -31,7 +31,7 @@ JSONSchemaPatternConstraint >> validate [ { #category : 'validation' } JSONSchemaPatternConstraint >> validate: aString [ - (pattern asRegex search: aString) ifFalse: [ + (pattern asECMARegex search: aString) ifFalse: [ JSONConstraintError signal: aString, ' does not match pattern ', pattern ] ] diff --git a/source/JSONSchema-Core/JSONSchemaPatternPropertiesConstraint.class.st b/source/JSONSchema-Core/JSONSchemaPatternPropertiesConstraint.class.st index d245efe..341565e 100644 --- a/source/JSONSchema-Core/JSONSchemaPatternPropertiesConstraint.class.st +++ b/source/JSONSchema-Core/JSONSchemaPatternPropertiesConstraint.class.st @@ -36,7 +36,7 @@ JSONSchemaPatternPropertiesConstraint >> validate: aDictionary [ is Boolean true = accept all)." | resolvedPatterns | resolvedPatterns := patternProperties associations collect: [ :assoc | - { assoc key asRegex. self resolveVisitor visitSchemaSpec: assoc value } ]. + { assoc key asECMARegex. self resolveVisitor visitSchemaSpec: assoc value } ]. aDictionary keysAndValuesDo: [ :key :value | | matchedPattern | matchedPattern := false. diff --git a/source/JSONSchema-Core/String.extension.st b/source/JSONSchema-Core/String.extension.st index 971baf5..ef40eab 100644 --- a/source/JSONSchema-Core/String.extension.st +++ b/source/JSONSchema-Core/String.extension.st @@ -1,5 +1,13 @@ Extension { #name : 'String' } +{ #category : '*jsonschema-core' } +String >> asECMARegex [ + "Pharo's Regex engine doesn't support ECMA-262 non-capturing groups (?:...). They're + semantically equivalent to a plain capturing group for the match/no-match checks JSON + Schema patterns need (pattern/patternProperties), so translate before compiling." + ^ (self copyReplaceAll: '(?:' with: '(') asRegex +] + { #category : '*jsonschema-core' } String >> asJSONSchema [ ^ JSONSchema string From d1c43cdd8ba5bd7c8d60c7825f66311c877490e7 Mon Sep 17 00:00:00 2001 From: Norbert Hartl Date: Sat, 1 Aug 2026 12:25:33 +0200 Subject: [PATCH 2/3] Also support escaped dot inside a character class ([a-z\.]) Found while writing broader OpenAPI document-validator test coverage: the OAI meta-schema's component-name patternProperties keys use [a-zA-Z0-9\.\-_]+ (components.schemas/responses/parameters/etc, i.e. almost every object in the spec) - Pharo's Regex engine raised 'bad backslash escape' on the escaped dot inside the class, same root cause as the non-capturing-group issue in the previous commit. \. and . are equivalent inside a character class in every regex dialect, so asECMARegex now also strips the backslash there (tracking class-nesting so it only touches escapes actually inside [...]). Co-Authored-By: Claude Sonnet 5 --- .../JSONSchemaTests.class.st | 11 +++++++ source/JSONSchema-Core/String.extension.st | 29 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st b/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st index 99287b2..5861322 100644 --- a/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st +++ b/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st @@ -25,6 +25,17 @@ JSONSchemaTests >> testMultipleConstraintsOnAnyObject [ should: [ schema validate: '1234567' ] raise: JSONConstraintError ] +{ #category : 'tests' } +JSONSchemaTests >> testPatternPropertiesWithEscapedDotInCharacterClassIsSupported [ + "Regression: the OAI OpenAPI-3.0 meta-schema uses [a-zA-Z0-9\.\-_]+ for component-name + patternProperties keys (components.schemas/responses/parameters/etc.) - Pharo's Regex + engine raised 'bad backslash escape' on the escaped dot inside the character class." + | schema | + schema := JSONSchema fromString: '{"type":"object","patternProperties":{"^[a-zA-Z0-9\.\-_]+$":{"type":"string"}},"additionalProperties":false}'. + self shouldnt: [ schema validate: {'Foo.Bar-1_2'->'ok'} asDictionary ] raise: Error. + self should: [ schema validate: {'has space'->'ok'} asDictionary ] raise: JSONSchemaError +] + { #category : 'tests' } JSONSchemaTests >> testPatternPropertiesWithNonCapturingGroupIsSupported [ "Same regression as #testPatternWithNonCapturingGroupIsSupported, but for the diff --git a/source/JSONSchema-Core/String.extension.st b/source/JSONSchema-Core/String.extension.st index ef40eab..7016c77 100644 --- a/source/JSONSchema-Core/String.extension.st +++ b/source/JSONSchema-Core/String.extension.st @@ -2,10 +2,31 @@ Extension { #name : 'String' } { #category : '*jsonschema-core' } String >> asECMARegex [ - "Pharo's Regex engine doesn't support ECMA-262 non-capturing groups (?:...). They're - semantically equivalent to a plain capturing group for the match/no-match checks JSON - Schema patterns need (pattern/patternProperties), so translate before compiling." - ^ (self copyReplaceAll: '(?:' with: '(') asRegex + "Pharo's Regex engine doesn't accept some ECMA-262 syntax that real-world schemas use + (e.g. the OAI OpenAPI meta-schema), even though it is semantically equivalent for the + boolean match/no-match checks pattern/patternProperties need: + - non-capturing groups (?:...) - rewritten to a plain capturing group (...) + - an escaped literal dot \. inside a character class [...] - rewritten to a bare . (both + mean 'literal dot' inside a class; Pharo's engine only accepts the unescaped form there)" + ^ self asECMARegexSource asRegex +] + +{ #category : '*jsonschema-core' } +String >> asECMARegexSource [ + "See #asECMARegex. Kept separate so the rewritten source itself is testable/inspectable." + | source result inClass i char | + source := self copyReplaceAll: '(?:' with: '('. + result := WriteStream on: String new. + i := 1. + inClass := false. + [ i <= source size ] whileTrue: [ + char := source at: i. + (char == $[ and: [ inClass not ]) ifTrue: [ inClass := true ]. + (char == $] and: [ inClass ]) ifTrue: [ inClass := false ]. + (inClass and: [ char == $\ and: [ i < source size and: [ (source at: i + 1) == $. ] ] ]) + ifTrue: [ result nextPut: $.. i := i + 2 ] + ifFalse: [ result nextPut: char. i := i + 1 ] ]. + ^ result contents ] { #category : '*jsonschema-core' } From cd0d8069b5e39f07711243b2a77e607eb6ea15c5 Mon Sep 17 00:00:00 2001 From: Norbert Hartl Date: Sat, 1 Aug 2026 14:30:22 +0200 Subject: [PATCH 3/3] Fix invalid JSON in the regex regression tests (single vs double backslash) The three new regex-regression tests embed a JSON string containing a literal backslash (the regex escape char). JSON itself requires that backslash to be escaped as \\ - I'd mirrored these from the working image with single backslashes instead, which is invalid JSON and raised NeoJSONParseError on a fresh load. Verified the corrected literals byte-for-byte via a real JSON parser before committing. Co-Authored-By: Claude Sonnet 5 --- source/JSONSchema-Core-Tests/JSONSchemaTests.class.st | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st b/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st index 5861322..44681d4 100644 --- a/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st +++ b/source/JSONSchema-Core-Tests/JSONSchemaTests.class.st @@ -31,7 +31,7 @@ JSONSchemaTests >> testPatternPropertiesWithEscapedDotInCharacterClassIsSupporte patternProperties keys (components.schemas/responses/parameters/etc.) - Pharo's Regex engine raised 'bad backslash escape' on the escaped dot inside the character class." | schema | - schema := JSONSchema fromString: '{"type":"object","patternProperties":{"^[a-zA-Z0-9\.\-_]+$":{"type":"string"}},"additionalProperties":false}'. + schema := JSONSchema fromString: '{"type":"object","patternProperties":{"^[a-zA-Z0-9\\.\\-_]+$":{"type":"string"}},"additionalProperties":false}'. self shouldnt: [ schema validate: {'Foo.Bar-1_2'->'ok'} asDictionary ] raise: Error. self should: [ schema validate: {'has space'->'ok'} asDictionary ] raise: JSONSchemaError ] @@ -41,7 +41,7 @@ JSONSchemaTests >> testPatternPropertiesWithNonCapturingGroupIsSupported [ "Same regression as #testPatternWithNonCapturingGroupIsSupported, but for the patternProperties keyword (matches property *names*, separate code path/call site)." | schema | - schema := JSONSchema fromString: '{"type":"object","patternProperties":{"^[1-5](?:\d{2}|XX)$":{"type":"string"}},"additionalProperties":false}'. + schema := JSONSchema fromString: '{"type":"object","patternProperties":{"^[1-5](?:\\d{2}|XX)$":{"type":"string"}},"additionalProperties":false}'. self shouldnt: [ schema validate: {'200'->'ok'} asDictionary ] raise: Error. self shouldnt: [ schema validate: {'2XX'->'ok'} asDictionary ] raise: Error. self should: [ schema validate: {'999'->'ok'} asDictionary ] raise: JSONSchemaError @@ -53,7 +53,7 @@ JSONSchemaTests >> testPatternWithNonCapturingGroupIsSupported [ '^[1-5](?:\d{2}|XX)$' - Pharo's Regex engine used to raise 'Invalid lookaround expression ?:' on the (?:...) syntax, crashing any validation that hit it." | schema | - schema := JSONSchema fromString: '{"type":"string","pattern":"^[1-5](?:\d{2}|XX)$"}'. + schema := JSONSchema fromString: '{"type":"string","pattern":"^[1-5](?:\\d{2}|XX)$"}'. self shouldnt: [ schema validate: '200' ] raise: Error. self shouldnt: [ schema validate: '2XX' ] raise: Error. self should: [ schema validate: '999' ] raise: JSONConstraintError