Skip to content

Rule proposal: non-JSON examples/default schema annotations silently corrupt generated JSON Schema #440

Description

@mattiamanzati

Problem

Schema annotations like examples and default are typed parametrically against the schema's decoded Type (Annotations.Documentation<T> uses ReadonlyArray<T> / T). So on a Schema.BigInt, Schema.Symbol, or Schema.UndefinedOr(...) schema, annotating with 123n, Symbol.for("a"), or undefined is perfectly type-correct — but these values are not JSON-representable, and they poison JSON Schema / OpenAPI generation with no warning at the annotation site.

Verified against effect@4.0.0-beta.101 with TypeScript 7.0.2 --strict: tsc is completely silent, yet Schema.toJsonSchemaDocument emits a document containing raw bigint values, so JSON.stringify of the generated "JSON Schema" throws Do not know how to serialize a BigInt. The Effect team has already been bitten by this class of bug and now filters such values at generation time:

  • 38abd6799 — filter non-JSON values from schema examples and defaults, closes #5884 (#5888)
  • 44e0b0444 — JSONSchema: add missing options for target JSON Schema version, closes #5883 (#5885)

Whether the outcome is invalid output (as observed on this beta) or silent filtering (post-fix behavior), the user's carefully written examples either break serialization or just vanish from the generated OpenAPI document. The statically visible cases (bigint, symbol, undefined, function types) are cheap to catch where the annotation is written.

Reproduction

import { Schema } from "effect"

const Balance = Schema.BigInt.annotate({
  examples: [123n], // not JSON-representable
  default: 0n
})

const WithUndefined = Schema.UndefinedOr(Schema.Number).annotate({
  examples: [undefined, 1]
})

const WithSymbol = Schema.Symbol.annotate({
  examples: [Symbol.for("a")],
  default: Symbol.for("b")
})

// Same footgun via annotateKey on a struct property
const Account = Schema.Struct({
  balance: Schema.BigInt.annotateKey({ examples: [42n], default: 0n })
})

const doc = Schema.toJsonSchemaDocument(Balance)
JSON.stringify(doc)

tsc --strict reports nothing (exit 0). At runtime, toJsonSchemaDocument passes the bigints straight through into the draft-2020-12 document, and JSON.stringify(doc) throws TypeError: Do not know how to serialize a BigInt — the generated "JSON Schema" is not serializable, with no warning at the annotation site.

Proposed rule behavior

  • When: at .annotate(...) / .annotateKey(...) calls on Schema-typed receivers (and the curried Schema.annotate / Schema.annotateKey / Schema.annotateEncoded pipeables), when the annotation object has an examples or default property. Note: this targets the v4 API surface — v3's .annotations({...}) spelling does not exist in v4.
  • Check: for default and each element of examples, get the checker type of the supplied value expression; report when it contains bigint, symbol, undefined, or a callable type (union members included).
  • Report on: the offending value expression (or the examples/default property when the value is not syntactically decomposable).
  • Message: `examples`/`default` value of type 'bigint' is not a JSON value — it will be silently omitted from (or corrupt) the generated JSON Schema. Annotate the encoded side or provide a `jsonSchema` override.
  • Code fix (suggestion): none automatic; optionally offer to move the annotation to annotateEncoded when the encoded type is JSON-representable.
  • Must NOT flag:
    • schemas that override the jsonSchema annotation (the user has taken control of generation);
    • annotation values whose types are JSON-representable (string, number, boolean, null, plain arrays/objects of those);
    • other annotation keys (title, description, identifier, ...);
    • values typed any/unknown (avoid noise; only statically certain non-JSON kinds).
  • Relation to existing rules: complements preferSchemaOverJson and the schema* family (schemaNumber, schemaStructWithTag, ...) which target schema construction; none of them inspect annotation values, so there is no overlap.

Proposed rule name

  • schemaNonJsonAnnotation — matches the schema* prefix convention and names the defect (non-JSON annotation value) concisely. Recommended.
  • schemaNonJsonExamples — more specific, but understates that default is covered too.
  • nonJsonValueInSchemaAnnotation — most descriptive, but longer than the surrounding rule set's style.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions