Skip to content

fix(validate): keep a csharp spec default when the value is null - #169

Merged
rjrodger merged 2 commits into
mainfrom
claude/csharp-null-scalar-defaults
Oct 8, 2026
Merged

rjrodger merged 2 commits into
mainfrom
claude/csharp-null-scalar-defaults

Conversation

@rjrodger

@rjrodger rjrodger commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

The cause

typescript's validate keeps the spec default for a null value exactly as for an absent key, because getprop answers NONE for a null, and it still refuses a null where a type or $EXACT is required. csharp kept defaults only for an absent key (if (!exact && !cKeyExists)), with a comment saying typescript skips only an absent key, which is not what getprop does. So validate({retries: null}, {retries: 3}) threw Expected field retries to be integer, but found no value where typescript gives {retries: 3}, and the same for a null under a map or list default. A $ONE alternative hit the same step with the NONE sentinel its nested call passes down for an absent or null value: validate({a: null}, {a: ['$ONE', 5, '$STRING']}) was refused where typescript gives {a: null}.

voxgig/sdkgen has carried a narrower version of this as a marked local patch in its vendored copy since 2026-09-23 (PATCH (scalar defaults, pending upstream fix)), covering scalar defaults only. Landing it here lets the next sdk checkpoint carry the fix and sdkgen's resync drop the patch.

Change

csharp/Struct.cs: a null value, and a NONE, keep the default as an absent key does (!cKeyExists || cval == null || ReferenceEquals(cval, NONE)). For a node default this matches typescript as well: a null map keeps its default map, and its required children are still checked. Exact mode is untouched.

Tests

ValidateNullScalarDefaults in csharp/tests/StructTest.cs pins typescript's answers, each read off typescript/dist:

  • defaults kept for an absent key, a partial map, all-null scalars, a nested null, a list of nulls, a null map and a null list; overrides of empty, zero and false kept;
  • $ONE with a plain default alternative: a null value ({a: null}), an absent key ({}), a null map holding one ({m: {}}) and a null slot in a list of them ({a: []});
  • refused: an empty list where a scalar default is declared, a required $STRING/$NUMBER/$BOOLEAN absent or null, a null map whose default has a required child, a null list slot under a required type, and $EXACT absent or null.

Controls

  • On main's Struct.cs the test fails on the five null cases: null, nested, list, null-map and null-list.
  • With the null condition alone, and not the NONE one, it fails on the four $ONE cases.
  • With both, dotnet test passes 91 of 91, the corpus suites included, and make -C csharp lint is clean.

Since review

A second Claude reviewer drove 154 shapes through typescript, csharp main and this branch: the change moves 58 of them, 57 to typescript's exact output (error messages included) and none away from it. It found the NONE gap above, which predates this PR. Adding ReferenceEquals(cval, NONE) fixed 9 more shapes with no other change, and f7888cc carries it, with the four $ONE cases. It also noted:

  • the comment gate reads no .cs file, and make -C csharp lint builds and formats the library project alone, so neither looks at the new test;
  • a resync of sdkgen moves one row of its structnull.test.ts: csharp's validate answer for a null under a map default becomes default, as ts, js, go, py, perl and rb already answer.

What it found outside this change is #170: php refuses a null under a default as csharp did, three more null cases where csharp differs from typescript, and the corpus's null rewrite that hides the class from every port. Codex reviewed bb4a4de and raised nothing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BoEed9CaF6sTNMcXxQaH6w

typescript's getprop answers NONE for a null value, so validate keeps the
spec default for a null exactly as for an absent key, and still refuses
a null where a type or $EXACT is required. csharp kept defaults only for
an absent key: validate({retries: null}, {retries: 3}) threw "Expected
field retries to be integer, but found no value" where typescript gives
{retries: 3}, and the same for a null under a map or list default.

ValidateNullScalarDefaults pins the typescript answers for absent, null,
nested, list, map and override values, and the required and $EXACT
refusals; on the old condition it fails on every null case.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BoEed9CaF6sTNMcXxQaH6w
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T13:47:53.632370Z bb4a4de PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

A $ONE alternative validates its value through a nested call whose data
is NONE when the key is absent or null, and csharp treated that NONE as a
present value: validate({a: null}, {a: [$ONE, 5, $STRING]}) refused it
where typescript gives {a: null}. The default step now treats NONE as it
treats an absent key or a null.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BoEed9CaF6sTNMcXxQaH6w
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