Skip to content

encoding/json + builtin/type.ts: correctness bugs #142

Description

@mck

Follow-up to #141 (three gs/builtin/slice.ts perf fixes, already opened).

While transpiling and running a real ~100k-LOC Go package (interpreter + parser + lexer + types + resolver for a design-tokens expression language) through goscript, we found a handful of additional bugs — mostly in the bundled encoding/json runtime, plus one more builtin/type.ts correctness bug. These are currently vendored locally as a --gs-path override (for encoding/json) and a post-generation text patch (for builtin/type.ts), rather than proposed as PRs yet, to keep review units small. Filing this as a pointer in case there's interest — happy to open individual PRs for any/all of these on request.

Summary of what we found (all root-caused with minimal standalone repros, transpiled and run under Bun):

  1. encoding/json omitempty not honored on marshal. Struct fields tagged ,omitempty were emitted unconditionally even when holding a Go zero value (zero number, empty string, false, empty slice/map, nil pointer/interface).

  2. encoding/json marshal leaks the boxed interface{} wrapper. A value carrying goscript's boxed-any wrapper (__goValue) serialized as {"__goValue": ...} instead of the underlying primitive/map/slice.

  3. encoding/json unmarshal doesn't type-drive Map/Slice struct fields. Only direct []Struct fields decoded into real struct instances; map[string]Struct / map[string][]Struct fields (and further nesting) left plain JS objects/arrays in place, so generated struct-field-accessor getters returned undefined.

  4. encoding/json unmarshal doesn't recurse into array elements inside interface{}-typed values. An interface{}-typed array of objects (e.g. Go []any holding map[string]any elements) left element objects as raw plain JS objects instead of the Map representation used elsewhere, breaking Go-side case map[string]any: type switches that call .entries().

  5. encoding/json unmarshal ignores json:"..." tags on anonymous (inline, unnamed) struct types. Anonymous struct targets carry no _fields/registered type metadata, so they fell through to the untyped interface{} decode path, which replaces the plain-object struct value with a Map — every field then read back empty.

  6. encoding/json RawMessage not handled as a map/slice element type, and no Pointer-to-struct branch in the type-driven decoder (map/slice element and field types like map[string]*Property leaked plain objects with undefined Go field reads).

  7. builtin/type.ts structural type matching misclassifies a plain JS Array as map[string]any. matchesMapType and typeAssert's inline map check accept any non-null object; Object.entries() of an array yields string indices that pass key/elem sampling. A Go type switch listing case map[string]any: before case []any: then captures arrays in the map branch, and ranging over the "map" produces numeric keys instead of the correct slice semantics — corrupting []any values into {0: ..., 1: ...}-shaped maps.

Each of these was found because our workload exercises real nested JSON documents (design-token specs) through encoding/json-based Unmarshal/Marshal and Go-side type switches on interface{} — not synthetic. Happy to share minimal repros or open PRs for any of these if useful; didn't want to dump 7 unrelated changes into one giant PR.

🤖 Filed with Claude Code

Metadata

Metadata

Assignees

Labels

Type

Fields

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions