Skip to content

Preserve colliding definitions when sanitizing JsonSchema keys - #6905

Closed
fubhy wants to merge 1 commit into
mainfrom
audit/repro-core-jsonschema-component-collision
Closed

Preserve colliding definitions when sanitizing JsonSchema keys#6905
fubhy wants to merge 1 commit into
mainfrom
audit/repro-core-jsonschema-component-collision

Conversation

@fubhy

@fubhy fubhy commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Distinct canonical definitions whose names sanitize to the same OpenAPI component key are silently merged. One schema is discarded and references to both definitions alias the surviving component.

Important

This PR starts with focused failing reproduction tests. Add the implementation fix to this same branch; CI is expected to fail until that fix is included.

OpenAPI component-key sanitization merges definitions

Module: JsonSchema
Audit ID: core-g-r-jsonschema-component-key-collision
Severity / confidence: high / high

What happens

Distinct canonical definitions whose names sanitize to the same OpenAPI component key are silently merged. One schema is discarded and references to both definitions alias the surviving component.

Why it happens

Invalid key characters are independently replaced with underscores, but the converter neither detects nor resolves collisions. A$B and A B both become A_B, and Rec.mapEntries emits only one property.

Expected behavior

toMultiDocumentOpenApi3_1 converts every canonical definition into a valid OpenAPI component and rewrites references to the corresponding converted definitions.

Relevant implementation

These links and excerpts are pinned to audit base c9b56ab507f224426ee8388dc450da447ec4715f.

View problematic code at packages/effect/src/JsonSchema.ts:669-699
export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-2020-12">): MultiDocument<"openapi-3.1"> {
  const keyMap = new Map<string, string>()
  for (const key of Object.keys(multiDocument.definitions)) {
    const sanitized = sanitizeOpenApiComponentsSchemasKey(key)
    if (sanitized !== key) {
      keyMap.set(key, sanitized)
    }
  }

  function rewrite(schema: JsonSchema): JsonSchema {
    return rewrite_refs(schema, ($ref) => {
      const tokens = $ref.split("/")
      if (tokens.length > 0) {
        const identifier = unescapeToken(tokens[tokens.length - 1])
        const sanitized = keyMap.get(identifier)
        if (sanitized !== undefined) {
          $ref = tokens.slice(0, -1).join("/") + "/" + sanitized
        }
      }
      return $ref.replace(RE_DEFS, "#/components/schemas")
    }) as JsonSchema
  }

  return {
    dialect: "openapi-3.1",
    schemas: Arr.map(multiDocument.schemas, rewrite),
    definitions: Rec.mapEntries(
      multiDocument.definitions,
      (definition, key) => [keyMap.get(key) ?? key, rewrite(definition)]
    )
  }

View exact lines on GitHub

View problematic code at packages/effect/src/JsonSchema.ts:703-735
export const VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP = /^[a-zA-Z0-9.\-_]+$/

/**
 * Returns a sanitized key for an OpenAPI component schema.
 * Should match the `^[a-zA-Z0-9.\-_]+$` regular expression.
 *
 * @internal
 */
export function sanitizeOpenApiComponentsSchemasKey(s: string): string {
  if (s.length === 0) return "_"
  if (VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP.test(s)) return s

  const out: Array<string> = []

  for (const ch of s) {
    const code = ch.codePointAt(0)
    if (
      code !== undefined &&
      ((code >= 48 && code <= 57) || // 0-9
        (code >= 65 && code <= 90) || // A-Z
        (code >= 97 && code <= 122) || // a-z
        code === 46 || // .
        code === 45 || // -
        code === 95) // _
    ) {
      out.push(ch)
    } else {
      out.push("_")
    }
  }

  return out.join("")
}

View exact lines on GitHub

Reproduction

pnpm vitest run packages/effect/test/JsonSchema.test.ts -t "preserves definitions whose keys sanitize to the same component name"

Observed failure: The intended failure was reproduced: one definition was emitted instead of two.

Implementation handoff

The initial reproduction tests on this branch are the regression specification for the implementation fix that should follow in this PR.

  1. Start with the pinned implementation excerpts and the Why it happens analysis above.
  2. Change the implementation so it satisfies the stated Expected behavior; do not weaken or remove the reproduction assertions.
  3. Run the focused reproduction command(s) and confirm the observed failures become passing tests:
pnpm vitest run packages/effect/test/JsonSchema.test.ts -t "preserves definitions whose keys sanitize to the same component name"
  1. Run the affected package's existing tests, then the repository lint and type checks before requesting review.

Audit provenance

  • Audit base: c9b56ab507f224426ee8388dc450da447ec4715f
  • Reproduction base: c9b56ab507f224426ee8388dc450da447ec4715f
  • Findings: core-g-r-jsonschema-component-key-collision
  • Initial patch: focused reproduction tests; implementation fix pending

@github-project-automation github-project-automation Bot moved this to Discussion Ongoing in PR Backlog Aug 3, 2026
@effect-slopcop effect-slopcop Bot added the 4.0 label Aug 3, 2026

@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

  • Reproduction test for component-key collision — adds a test that exposes the bug where sanitizeOpenApiComponentsSchemasKey produces identical keys ("A_B") for distinct definition keys ("A$B" and "A B"), causing one definition to be dropped.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) (Kimi K2 not used — the program covers this model; add its provider key to run your pick) | 𝕏

@gcanti

gcanti commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixed in #6883

@gcanti gcanti closed this Aug 3, 2026
@github-project-automation github-project-automation Bot moved this from Discussion Ongoing to Done in PR Backlog Aug 3, 2026
@fubhy fubhy changed the title Add reproduction for JsonSchema issue Preserve colliding definitions when sanitizing JsonSchema keys Aug 3, 2026
@effect-slopcop effect-slopcop Bot added the bug Something isn't working label Aug 3, 2026
@fubhy fubhy added the audit Findings originating from the Effect runtime correctness audit label Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.0 audit Findings originating from the Effect runtime correctness audit bug Something isn't working

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants