Skip to content

Fix mixed index signatures lose their broad constraint - #7059

Open
fubhy wants to merge 2 commits into
mainfrom
audit/repro-17f0b91a-json-schema-mixed-index-signatures
Open

Fix mixed index signatures lose their broad constraint#7059
fubhy wants to merge 2 commits into
mainfrom
audit/repro-17f0b91a-json-schema-mixed-index-signatures

Conversation

@fubhy

@fubhy fubhy commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

Exporting an object representation with both a broad string index signature and a template-literal index signature drops the broad constraint whenever patternProperties is emitted. Non-pattern keys then become unconstrained, and pattern keys no longer satisfy both source index constraints.

Note

This PR includes the focused regression test and the implementation fix.

Mixed index signatures lose their broad constraint

Module: effect/internal/schema/toJsonSchemaDocument
Audit ID: effect-bc04d519737af4ef
Severity / confidence: medium / high

What happens

Exporting an object representation with both a broad string index signature and a template-literal index signature drops the broad constraint whenever patternProperties is emitted. Non-pattern keys then become unconstrained, and pattern keys no longer satisfy both source index constraints.

Why it happens

The compiler stores a patternless string signature in out.additionalProperties and patterned signatures in patternProperties. After assigning out.patternProperties, it unconditionally deletes out.additionalProperties, erasing the previously compiled broad signature.

Expected behavior

SchemaRepresentation.toJsonSchemaDocument must encode simultaneous broad and patterned object index signatures conjunctively, retaining the broad schema in additionalProperties alongside patternProperties.

Relevant implementation

These links and excerpts are pinned to audit base 17f0b91a243ccfe4a38d27debdc983adf434e738.

View problematic code at packages/effect/src/internal/schema/toJsonSchemaDocument.ts:342-391
      case "Objects": {
        if (representation.propertySignatures.length === 0 && representation.indexSignatures.length === 0) {
          return { anyOf: [{ type: "object" }, { type: "array" }] }
        }
        const out: JsonSchema.JsonSchema = { type: "object" }
        const properties: Record<string, JsonSchema.JsonSchema> = {}
        const required: Array<string> = []
        for (let index = 0; index < representation.propertySignatures.length; index++) {
          const property = representation.propertySignatures[index]
          if (typeof property.name !== "string") {
            throw errorWithPath("Invalid schema representation document", [
              ...path,
              "propertySignatures",
              index,
              "name"
            ])
          }
          const name = property.name
          const compiled = recur(property.type, [...path, "propertySignatures", index, "type"])
          const annotations = collectJsonSchemaAnnotations(property.annotations, options)
          InternalRecord.assignProperty(
            properties,
            name,
            annotations === undefined ? compiled : appendJsonSchema(compiled, annotations)
          )
          if (!property.isOptional) required.push(name)
        }
        if (representation.propertySignatures.length > 0) out.properties = properties
        if (required.length > 0) out.required = required
        out.additionalProperties = options?.additionalProperties ?? false
        const patternProperties: Record<string, JsonSchema.JsonSchema | false> = {}
        for (let index = 0; index < representation.indexSignatures.length; index++) {
          const signature = representation.indexSignatures[index]
          let type: JsonSchema.JsonSchema | false = recur(
            signature.type,
            [...path, "indexSignatures", index, "type"]
          )
          if (Object.keys(type).length === 1 && "not" in type) type = false
          const patterns = getParameterPatterns(
            signature.parameter,
            [...path, "indexSignatures", index, "parameter"],
            new Set()
          )
          if (patterns.length === 0) {
            out.additionalProperties = type
          } else {
            for (const pattern of patterns) InternalRecord.assignProperty(patternProperties, pattern, type)
          }
        }
        if (Object.keys(patternProperties).length > 0) {

View exact lines on GitHub

Excerpt truncated. Open the complete packages/effect/src/internal/schema/toJsonSchemaDocument.ts:342-402 range.

Reproduction

pnpm test --run packages/effect/test/schema/representation/toJsonSchemaDocument.test.ts

Observed failure: Focused contract assertion failed against 17f0b91, demonstrating: Mixed index signatures lose their broad constraint.

Validation

The original failing reproduction now passes with the implementation fix:

pnpm test --run packages/effect/test/schema/representation/toJsonSchemaDocument.test.ts

Audit provenance

  • Audit base: 17f0b91a243ccfe4a38d27debdc983adf434e738
  • Reproduction base: 17f0b91a243ccfe4a38d27debdc983adf434e738
  • Findings: effect-bc04d519737af4ef
  • Initial patch: focused reproduction tests; implementation fix added in 99934ba49

Closes EFF-492

@fubhy fubhy added the audit Findings originating from the Effect runtime correctness audit label Aug 5, 2026
@changeset-bot

changeset-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 99934ba

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
effect Patch
@effect/ai-anthropic Patch
@effect/ai-openai Patch
@effect/ai-openai-compat Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch
@effect/opentelemetry Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node Patch
@effect/platform-node-shared Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/vitest Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

This PR currently adds a failing regression test but does not include the implementation fix. The test correctly demonstrates the bug, but the source change in toJsonSchemaDocument.ts is still needed before this can land.

Reviewed changes

  • Added a focused regression test in packages/effect/test/schema/representation/toJsonSchemaDocument.test.ts that compiles an Objects representation with both a broad string index signature and a template-literal index signature, asserting that the resulting JSON Schema has both patternProperties and additionalProperties.

⚠️ Implementation fix is missing

The PR description accurately identifies the bug in packages/effect/src/internal/schema/toJsonSchemaDocument.ts at the Objects branch: after assigning out.patternProperties, the code unconditionally deletes out.additionalProperties, which drops the broad index signature. The fix must retain additionalProperties when it was set by a broad index signature, while continuing to omit the default false for object representations that have only pattern signatures.

Be careful not to regress the existing pure-pattern test at packages/effect/test/schema/representation/toJsonSchemaDocument.test.ts:698, which expects { type: "object", patternProperties: { "^a": { type: "string" } } } without additionalProperties.

Technical details
# Mixed index signature fix

## Affected sites
- `packages/effect/src/internal/schema/toJsonSchemaDocument.ts:391-393` — unconditional `delete out.additionalProperties` after emitting `patternProperties`.

## Required outcome
- When an `Objects` representation contains a broad string index signature and one or more pattern index signatures, the emitted JSON Schema must keep the broad type in `additionalProperties` and the pattern types in `patternProperties`.
- When there is no broad index signature, the current behavior of omitting `additionalProperties` (which is the default `false`) must remain unchanged so existing tests keep passing.

## Suggested approach
Track whether a broad index signature contributed to `out.additionalProperties` (e.g. `let hasBroadSignature = false` and set it when `patterns.length === 0`). Then delete `additionalProperties` only when `!hasBroadSignature` after `patternProperties` are emitted.

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Fix it ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

@tim-smart
tim-smart requested a review from gcanti August 5, 2026 23:44

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Added a changeset describing the fix as an effect patch.
  • Implemented the fix in packages/effect/src/internal/schema/toJsonSchemaDocument.ts by tracking whether a broad index signature contributed to out.additionalProperties; additionalProperties is now only deleted when patternProperties are emitted if no broad signature exists, preserving the broad constraint for mixed index signatures while keeping pure-pattern behavior unchanged.

The prior review's main concern — the missing implementation fix — is now addressed. Validation passed: the focused toJsonSchemaDocument test file, the related toJsonSchemaMultiDocument test file, pnpm --filter effect check, and pnpm lint-fix all succeed.

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
basic.ts 7.06 KB 7.06 KB 0.00 KB (0.00%)
batching.ts 9.86 KB 9.86 KB 0.00 KB (0.00%)
brand.ts 6.34 KB 6.34 KB 0.00 KB (0.00%)
cache.ts 10.71 KB 10.71 KB 0.00 KB (0.00%)
config.ts 20.73 KB 20.73 KB 0.00 KB (0.00%)
differ.ts 20.31 KB 20.31 KB 0.00 KB (0.00%)
http-client.ts 21.53 KB 21.53 KB 0.00 KB (0.00%)
logger.ts 10.84 KB 10.84 KB 0.00 KB (0.00%)
metric.ts 8.98 KB 8.98 KB 0.00 KB (0.00%)
optic.ts 7.18 KB 7.18 KB 0.00 KB (0.00%)
pubsub.ts 14.99 KB 14.99 KB 0.00 KB (0.00%)
queue.ts 11.66 KB 11.66 KB 0.00 KB (0.00%)
schedule.ts 10.83 KB 10.83 KB 0.00 KB (0.00%)
schema-class.ts 19.27 KB 19.27 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 29.09 KB 29.09 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 25.40 KB 25.40 KB 0.00 KB (0.00%)
schema-string-transformation.ts 13.42 KB 13.42 KB 0.00 KB (0.00%)
schema-string.ts 10.95 KB 10.95 KB 0.00 KB (0.00%)
schema-template-literal.ts 15.21 KB 15.21 KB 0.00 KB (0.00%)
schema-toArbitraryLazy.ts 22.02 KB 22.02 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 24.45 KB 24.45 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 19.28 KB 19.28 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 19.11 KB 19.11 KB 0.00 KB (0.00%)
schema-toFormatter.ts 18.97 KB 18.97 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 22.70 KB 22.69 KB +0.01 KB (+0.04%)
schema-toRepresentation.ts 19.60 KB 19.60 KB 0.00 KB (0.00%)
schema.ts 18.52 KB 18.52 KB 0.00 KB (0.00%)
stm.ts 12.63 KB 12.63 KB 0.00 KB (0.00%)
stream.ts 9.80 KB 9.80 KB 0.00 KB (0.00%)

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

Labels

audit Findings originating from the Effect runtime correctness audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants