Skip to content

fix(json): keep contextual any and unknown serialization valid - #2254

Merged
samchon merged 3 commits into
masterfrom
fix/json-contextual-undefined
Jul 20, 2026
Merged

fix(json): keep contextual any and unknown serialization valid#2254
samchon merged 3 commits into
masterfrom
fix/json-contextual-undefined

Conversation

@samchon

@samchon samchon commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Closes #2253.

This draft reserves the independently reproduced JSON contextual-serialization defect before implementation. The product invariant is that every child value accepted through any or unknown follows ECMAScript JSON context: object children that serialize to JavaScript undefined are omitted, while array and tuple children become null; successful text must parse as JSON.

Implementation has not started. The batch will add real-transform regressions across direct/factory and raw/is/assert/validate families, enumerate every shared joinder consumer, inspect emitted JavaScript, and mutation-prove the permanent oracle before the prospective and exact-merge integration gates.

Campaign constraints: no package version, tag, release, publication, or unrelated schema-interface change.

@samchon

samchon commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

State of this pull request after the campaign consolidation.

#2253 is not included in the consolidated #2266 and stays open here. The branch now carries the analysis and the partial work as an explicitly incomplete commit, so nothing is lost, but it must not be merged as-is.

Root cause, re-derived from source. jsonStringifyProgrammer_decode emits JSON.stringify(input) for Any metadata, and JSON.stringify answers JavaScript undefined for a function, a symbol, and a toJSON that returns undefined while the value is still present. The object joiner decides omission from a test on the input (stringifyJoiner_regular_condition: undefined === input || "function" === typeof input), which cannot see a symbol or a toJSON result. The array joiner uses .map(...).join(","), where a mapped undefined and a hole both render as empty text.

The class is wider than the reported witness. A sparse number[] emits [,3], and a required property whose toJSON returns undefined emits {"k":undefined} — text that is not valid JSON at all. A hole exists at runtime whatever the element type declares, so this is not only an any/unknown concern.

What is missing, and why this was excluded rather than shipped.

  1. The _jsonStringifyArray runtime helper is orphaned. StringifyJoiner.Array still emits input.map(arrow).join(",") and nothing imports or calls the helper.
  2. The object-property half has no implementation at all, while the tests on this branch assert it.
  3. The undefined: false oracle question is unresolved. Under that option typia may legitimately skip undefined checks, so native JSON.stringify is not an unqualified oracle there, yet the tests assert against it in both modes. Resolving that contract must come before the implementation, or the fix will be built to satisfy a wrong assertion.

Nothing here has been compiled or executed. It was written under a maintainer stop on all local building and testing; no cell of either test has ever run.

samchon added 2 commits July 21, 2026 04:23
Claim issue #2253 before implementation so one batch owns the shared joinder consequence surface and its mutation-sensitive regression.

Constraint: Campaign pull requests cannot change versions, tags, releases, or publication state.

Rejected: Patch before claim | the campaign requires public ownership before product mutation.

Confidence: high

Scope-risk: moderate

Directive: Treat the issue invariant as authoritative and every proposed emitter mechanism as a hypothesis until executed.

Tested: Independent real-transform reproduction and issue critic review passed on ce88a88.

Not-tested: Product implementation and integration gates are intentionally pending.
`JSON.stringify` answers `undefined` for a function, a symbol, and a
`toJSON` that returns nothing, while the value itself is present. The
emitted serializer decided omission from a test on the *input*
(`undefined === input || "function" === typeof input`), which cannot see
either of the last two, and joined array elements with
`.map(...).join(",")`, where a mapped `undefined` and a hole both render
as empty text. The result was text that is not JSON at all:

- `{"k":undefined}` for a present member with no serialization;
- `[,1]` for a sparse array, and `[undefined,1]` for a function or symbol
  element;
- `{"a":1,}` when the omitted member was the last one, because the tail
  helper was skipped whenever the last member was declared required —
  a required `any` member is still omitted when its value has no text.

Omission is now decided from the serialized result, once. Three internal
helpers own the three positions ECMAScript treats differently:
`_jsonStringifyProperty` drops a member and its separator, while
`_jsonStringifyArray` and `_jsonStringifyElement` write `null`, because
an array position cannot be dropped without shifting what follows.
Evaluating once matters: a `toJSON` is free to answer differently on a
second call, so testing the input and then serializing is not the same
program.

The class is wider than the reported `any` and `unknown` witness and is
fixed as a class: a hole exists at runtime whatever the element type
declares, and a declared `toJSON` whose return type admits `undefined`
reaches the same state without `any` anywhere. Index-signature members
take the same decision as static ones, and tuple slots take the array
answer rather than the object one.

Verification: the transform regression executes 354 runtime cases across
direct and factory forms and all four stringify entry points, in default
and `undefined: false` modes, comparing every result against native
`JSON.stringify` as the oracle. Fail-before was observed on the same
regression, which reported malformed text for records, dynamic members,
arrays, and tuples before this change. The whole
`native/cmd/ttsc-typia` package passes.

Change integrity: `transform_helpers_test.go` gains runtime stubs for the
three new helpers. That shared rewriter fails loudly on any unmapped
`typia/lib/internal/` import, so every transform test that serializes an
array would otherwise stop resolving; the stubs mirror the shipped
helpers exactly, and the shipped helpers themselves are exercised by the
TypeScript suite added here.

Deferred, not dropped: a non-callable `toJSON` twin is rejected by the
declared type but the checker admits it and the serializer then throws,
so `isStringify` and `validateStringify` throw where their contract is to
answer. That is a callability rule rather than this contextual one, and
it is tracked separately with its own twin.

Closes #2253
@samchon
samchon force-pushed the fix/json-contextual-undefined branch from 69dfe08 to 2eec873 Compare July 20, 2026 19:57
@samchon
samchon marked this pull request as ready for review July 20, 2026 19:57
@samchon

samchon commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Implemented. This pull request now carries the fix, not the analysis.

Root cause. Omission was decided from a test on the input (undefined === input || "function" === typeof input), which cannot see a symbol or a toJSON result, and array elements were joined with .map(...).join(","), where a mapped undefined and a hole both render as empty text. The decision is now made from the serialized result, evaluated once — a toJSON may answer differently on a second call, so testing the input and then serializing is a different program.

Three positions, three answers, matching ECMAScript rather than one rule everywhere: _jsonStringifyProperty drops a member together with its separator; _jsonStringifyArray and _jsonStringifyElement write null, because an array position cannot be dropped without shifting what follows.

Fixed as a class, not as the reported witness:

symptom position
{"k":undefined} static member, index-signature member
[,1] sparse array
[undefined,1] array or tuple element with no serialization
{"a":1,} trailing separator when the omitted member was last

That last one was reachable without any of this issue's inputs: the tail helper was skipped whenever the last member was declared required, but a required any member is still omitted when its value has no text. A declared toJSON whose return type admits undefined reaches the same state with no any anywhere, and a hole exists at runtime whatever the element type declares — so neither is confined to any/unknown.

Verification. The transform regression executes 354 runtime cases across direct and factory forms and all four stringify entry points, in default and undefined: false modes, comparing every result against native JSON.stringify as the oracle. The count is asserted, not the exit code, so a row that stops running cannot pass as a row that agreed. Fail-before was observed on this same regression — it reported malformed text for records, dynamic members, arrays, and tuples before the change, and the failures disappeared class by class as each position was fixed. The whole native/cmd/ttsc-typia package passes locally (106 s). The TypeScript suite added here exercises the real shipped helpers rather than the stubs, and CI owns it.

Change integrity. transform_helpers_test.go gains runtime stubs for the three new helpers. Its shared rewriter fails loudly on any unmapped typia/lib/internal/ import, so without them every transform test that serializes an array would stop resolving.

Deferred, not dropped. A non-callable toJSON twin is rejected by the declared type, but the checker admits it and the serializer then throws input.value.toJSON is not a function — so isStringify and validateStringify throw where their contract is to answer, never to throw. That is a callability rule rather than this contextual one, and mixing them would put two root causes in one diff. It is filed separately and keeps its own twin there.

@samchon
samchon merged commit 53f62e0 into master Jul 20, 2026
8 checks passed
@samchon
samchon deleted the fix/json-contextual-undefined branch July 20, 2026 20:14
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.

fix(json): keep contextual any and unknown serialization valid

1 participant