Skip to content

fix(json): make JSON Schema enum compile again - #572

Merged
wolfy-j merged 1 commit into
mainfrom
fix/json-schema-enum-compile
Aug 19, 2026
Merged

fix(json): make JSON Schema enum compile again#572
wolfy-j merged 1 commit into
mainfrom
fix/json-schema-enum-compile

Conversation

@wolfy-j

@wolfy-j wolfy-j commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Problem

JSON Schemas containing enum fail during compilation in official builds:

json: cannot unmarshal into Go *interface {} within "/properties/state/enum/0": unsupported operation

kaptinlin/jsonschema uses the standalone go-json-experiment/json unmarshaler fallback contract. With GOEXPERIMENT=jsonv2, that module becomes an alias over the standard-library implementation, which uses a different, unexported skip sentinel. The dependency can no longer request default unmarshaling.

Resolution

  • Explicitly build with GOEXPERIMENT=nojsonv2 so schema compilation uses the supported standalone implementation.
  • Keep direct token-to-Lua decoding by importing github.com/go-json-experiment/json/jsontext directly.
  • Replace the build-tagged v1/v2 split with one decoder, avoiding behavioral drift between build modes.
  • Handle int64, larger JSON integers, floats, and unrepresentable numbers explicitly.
  • Use the SHA-256 value directly as the schema-cache key instead of allocating a hex string.
  • Cover enum compilation, invalid enum values, empty object identity, and numeric boundaries.

Performance

The fix does not retain the slower intermediate map[string]any decoder. Representative results on an AMD Ryzen 9 7950X3D, -benchtime=500ms, five runs:

Benchmark Median Bytes/op Allocs/op
Decode simple 612 ns 1,017 18
Decode complex 2.49 µs 3,572 64
Decode large array 31.4 µs 55,394 721
Decode large dataset 1.37 ms 1,578,084 30,020
Decode nested config 11.7 µs 17,429 268

These allocation counts match the previous experimental fast decoder. Cached enum validation is approximately 2.2–2.9 µs at 2,443 bytes and 32 allocations per operation. Allocation profiling attributes the remaining validation cost primarily to kaptinlin/jsonschema evaluation-result construction.

Verification

  • make lint
  • make test-runtime
  • JSON package race suite, 20 repetitions
  • go mod tidy -diff
  • Decode and cached enum-validation benchmarks

@wolfy-j
wolfy-j force-pushed the fix/json-schema-enum-compile branch from e855c4f to 4153b40 Compare August 15, 2026 22:35
@wolfy-j
wolfy-j force-pushed the fix/json-schema-enum-compile branch from 4153b40 to e007a01 Compare August 19, 2026 14:28
Any JSON Schema carrying an `enum` currently fails to compile, before any
data is examined:

    json: cannot unmarshal into Go *interface {} within
    "/properties/state/enum/0": unsupported operation

Neither this repo nor github.com/kaptinlin/jsonschema is at fault.
github.com/go-json-experiment/json ships two implementations behind build tags
which disagree on how a custom unmarshaler signals "skip me":

  !goexperiment.jsonv2          arshal_funcs.go   errors.ErrUnsupported
  goexperiment.jsonv2 && go1.25 alias.go          stdlib SkipFunc, not re-exported

kaptinlin/jsonschema returns errors.ErrUnsupported from the *any unmarshaler it
installs for JSON number handling (compiler.go:21-24), exactly as the standalone
package documents. Under the alias, encoding/json/v2 treats that as fatal, and
json.SkipFunc is not reachable through the alias at all -- substituting it in a
local copy of the dependency fails to compile with "undefined: json.SkipFunc".

So under the experiment a dependency cannot signal skip by any means. An enum
array is the common place a non-number value reaches that code path, which is
why enum is the keyword that surfaces it. The alias is not a faithful drop-in
for the package it replaces, and that is not fixable from here, so the
experiment is disabled until it is. Re-enabling is a one-line revert.

Upgrading the dependency does not help: v0.9.8 fails identically.

Disabling the experiment activates decode_v1.go, which was not behaviourally
equivalent to decode_v2.go: it sized the object table from the decoded map, so
an empty JSON object allocated no string dictionary and re-encoded as []. It now
allocates the dictionary the way decodeObject does on the v2 path.

Both behaviours are pinned by tests that fail before this change and pass after.

Cost: decode_v2.go imports stdlib encoding/json/jsontext and so requires the
experiment, meaning the fast decoder and working schema validation cannot
currently coexist. Measured on this branch at 200x --

    Simple         964ns  ->  1360ns
    Complex       2934ns  ->  5907ns
    LargeArray     33.9us ->  66.1us
    LargeDataset    1.29ms ->  2.78ms

roughly 1.4-2.2x on decode. A third option keeps both, at the cost of a
maintained fork: replace go-json-experiment/json with one whose alias.go build
tag is dropped, so dependencies keep the standalone semantics while this repo
keeps the experiment for its own jsontext use.

go test ./runtime/... ./internal/... ./api/... -short passes.
@wolfy-j
wolfy-j force-pushed the fix/json-schema-enum-compile branch from e007a01 to 8f0851c Compare August 19, 2026 14:35
@wolfy-j
wolfy-j merged commit 4ce436b into main Aug 19, 2026
6 of 7 checks passed
@wolfy-j
wolfy-j deleted the fix/json-schema-enum-compile branch August 19, 2026 14:53
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.

2 participants