Skip to content

fix(schema): infer patternProperties value type for any single pattern#3027

Open
schani wants to merge 8 commits into
masterfrom
agent/fix-issue-1464
Open

fix(schema): infer patternProperties value type for any single pattern#3027
schani wants to merge 8 commits into
masterfrom
agent/fix-issue-1464

Conversation

@schani

@schani schani commented Jul 20, 2026

Copy link
Copy Markdown
Member

Bug

patternProperties in JSON Schema was only correctly narrowed to the
declared value type for the specific pattern ".*". Any other single
regex (e.g. ^[a-zA-Z]\w*$) fell back to an untyped map value (e.g.
nlohmann::json / any in C++), even though the schema explicitly
declared the property type.

Repro:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "patternProperties": {
    "^[a-zA-Z]\\w*$": { "type": "boolean" }
  }
}
node dist/index.js --src-lang schema -l cpp schema.json

Before:

using Schema = std::map<std::string, nlohmann::json>;

Expected/after:

using Schema = std::map<std::string, bool>;

Root cause

packages/quicktype-core/src/input/JSONSchemaInput.ts has a hack
(originally added for #976) that treats patternProperties as
additionalProperties — but only when the pattern is the literal string
".*":

if (
    additionalProperties === undefined &&
    typeof schema.patternProperties === "object" &&
    hasOwnProperty(schema.patternProperties, ".*")
) {
    additionalProperties = schema.patternProperties[".*"];
}

Any other pattern (regardless of how narrow or specific) was ignored, so
the value type was never inferred from it.

Fix

Generalized the check to apply whenever patternProperties has exactly
one entry, using that entry's schema as additionalProperties
regardless of what the pattern string actually is. quicktype does not
validate property names against the pattern anyway, and with only one
pattern there's no ambiguity about which schema to use for the map's
value type. The pre-existing .* special case (and its fixture,
go-schema-pattern-properties.schema, covering #976) continues to work
unchanged since .* is itself just a single-entry case.

Schemas with more than one patternProperties entry are unaffected and
continue to fall back to any, as before — this change does not attempt
per-pattern-key typing for that case.

Test coverage

Added test/inputs/schema/pattern-properties.schema (the schema from
the issue) with:

  • pattern-properties.1.json — a valid sample ({"enabled": true})
  • pattern-properties.1.fail.json — an invalid sample ({"enabled": {}})
    that must fail to parse once the value type is correctly narrowed to
    boolean

This is an end-to-end JSON Schema fixture per the repo's testing
conventions (test/inputs/schema/), auto-discovered by the existing
schema fixture machinery — no changes to test/languages.ts /
test/fixtures.ts were needed.

Verification

  • npm run build passes.
  • Manually reproduced the bug before the fix (confirmed std::map<std::string, nlohmann::json> with an "cannot infer this type" warning) and confirmed the fix produces std::map<std::string, bool> with no warning.
  • Ran the isolated schema fixture for TypeScript against just the new input (QUICKTEST=true FIXTURE=schema-typescript script/test test/inputs/schema/pattern-properties.schema) — passes (both the positive and negative sample).
  • Confirmed the pre-existing .*-pattern fixture (go-schema-pattern-properties.schema, for Infer property type based on patternProperties if pattern is .* #976) still generates the correct typed map (std::map<std::string, int64_t>), i.e. no regression.
  • Ran the full schema-typescript fixture suite; it surfaced one unrelated pre-existing failure (vega-lite.schema, a TypeScript index-signature type error) that I confirmed also fails identically on unmodified master (no patternProperties involved in that schema at all) — not caused by this change.
  • Did not run the C++ fixture's full compile step locally — this sandbox lacks Boost headers (the C++ fixture defaults to boost: true), which is unrelated to this fix; CI will validate it.

Fixes #1464

🤖 Generated with Claude Code

schani and others added 7 commits July 20, 2026 17:58
#1464)

The additionalProperties/patternProperties hack added for #976 only kicked
in when the pattern was literally ".*", so any other single regex (e.g.
"^[a-zA-Z]\w*$") fell back to an untyped map value (nlohmann::json/any)
instead of the schema's declared type. Generalize the hack to apply
whenever patternProperties has exactly one entry, regardless of the
pattern text, since quicktype does not validate property names against
the pattern anyway.

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
The typescript-zod renderer emits no named schema export for a
top-level map type, so the zod test driver throws 'No schema found'.
This is a pre-existing zod limitation the new pattern-properties.schema
fixture (a top-level map) exposes; the plain typescript renderer handles
it fine. Scope the fixture out for zod.

Co-Authored-By: Claude <noreply@anthropic.com>
cjson does not validate patternProperties/map value types against the
input on parse (a known, documented limitation shared with
go-schema-pattern-properties.schema), so the new negative test case
never fails as expected. It also crashes (SIGSEGV) when printing a
scalar (bool) map value produced from this schema, since the map/array
item-printing code only special-cases string and object/union values.
Skip the new fixture for cjson, matching the existing precedent for
the same class of limitation, until that support is added.

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
Java deserializes a top-level map via a raw `Map.class` reader
(javaTypeWithoutGenerics drops the value type), so the Boolean map
value type is not enforced on parse and pattern-properties.1.fail.json
round-trips instead of failing. Skip the schema for Java; nested typed
maps keep their value-validation coverage elsewhere, and the schema's
positive and negative cases still run on other languages.

Co-Authored-By: Claude <noreply@anthropic.com>
…aren't enforced

The new pattern-properties.schema is a top-level map whose fail sample
`{"enabled": {}}` must be rejected. Languages that deserialize a map
without validating the value type (unchecked generic casts, raw map
readers, or no runtime type checks) round-trip it instead of failing, so
add pattern-properties.schema to the shared skipsMapValueValidation list.
That consolidates the previously per-language cjson and zod skips.

PHP additionally can't run the positive case: it has no type aliases, so
a top-level map produces no named TopLevel class and the driver's
TopLevel::from() call fails. Skip it there for the same reason top-level
unions, enums, and arrays are already skipped for PHP.

Positive and negative cases still run on many validating languages
(Go, Rust, Python, TypeScript, JavaScript, Flow, C#, C++, Scala, Elm,
Dart, KotlinX, schema-to-schema).

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Generated-output differences

16 files differ — 0 modified, 16 new, 0 deleted
1022 changed lines — +1022 / −0

Open the generated-output report →

@github-actions

Copy link
Copy Markdown

Generated-output differences

17 files differ — 0 modified, 17 new, 0 deleted
1077 changed lines — +1077 / −0

Open the generated-output report →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

patternProperties loses the type of the properties

1 participant