Skip to content

Say that a null is no string, except where an array or an object holds it - #2477

Merged
danielcweber merged 1 commit into
14.xfrom
fix/string-from-null-contract
Sep 30, 2026
Merged

danielcweber merged 1 commit into
14.xfrom
fix/string-from-null-contract

Conversation

@danielcweber

@danielcweber danielcweber commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

A JSON null that is asked for as a string is no string: at the top level it declines, as it does for an int?, a Uri or a Person, and it is never read as the empty string. Inside an array it stays a null, and inside an object it leaves the member it belongs to as it was. That is the answer this implementation already gives. Until now nothing pinned it, and the System.Text.Json implementation in Gremlinq.Extensions read such a null as "", so a vertex whose string property is null came back with an empty string in its place.

Tests only.

Changes

  • Tests.Infrastructure: thirteen contract tests, placed after Nullable_null_at_top_level, which says the same of an int?:
    • String_from_null, Uri_from_null and Person_from_null: at the top level there is nothing to hold a null, whatever the requested type.
    • String_from_single_item_array_with_null, String_from_typed_value_with_null, String_from_Property_with_null_value and String_from_VertexProperty_with_null_value: unwrapping a null does not make it a string.
    • Property_of_string_from_null and VertexProperty_of_string_from_null: a null is not the value of a property that a scalar is wrapped into either.
    • Strings_from_Array_with_null: in an array the null survives, as Nullable_null has it for an int?.
    • Constructor_arguments_from_null, Member_from_null and Language_with_null_property_value: a constructor argument is passed the null, and a member is left as it was, also when the null arrives inside a vertex property. A snapshot leaves out a member that is null and writes one that is empty, so these tell the two apart.

Notes

  • No source change and no behaviour change, so version.json is left alone.
  • Measured against the System.Text.Json implementation before its fix: it answered ten of the thirteen with the empty string in place of the null. Uri_from_null, Person_from_null and Strings_from_Array_with_null it already answered the same way. Its fix is Gremlinq.Extensions#214.
  • Measured on the way and deliberately not pinned, because the answers are not ones a contract should hold:
    • A Property<string> or VertexProperty<string> whose value is null throws the ArgumentNullException of its constructor, here and for any other value type that can hold a null (Property<int?>). Declining instead would need a change to this implementation.
    • A null value in a plain JSON object read as a Dictionary<string, string> or Dictionary<string, int?> is kept as a null entry, while in a g:Map the entry is left out; read as a Dictionary<string, int> the plain object throws an ArgumentNullException.
  • Support.NewtonsoftJson.Tests on net10.0: 312, up from 299, all green. dotnet build ExRam.Gremlinq.slnx succeeds with 0 warnings.

🤖 Generated with Claude Code

…s it

Nullable_null_at_top_level says a null asked for as an int? at the top level comes to
nothing: there is no enclosing structure to hold it, and TryTransform cannot report
one. Nothing said the same of a string, the one scalar that can hold a null without a
Nullable<> around it - nor what becomes of a null that is asked for as a string
further in.

Thirteen contract tests pin it:

- String_from_null, Uri_from_null and Person_from_null: at the top level there is
  none, whatever the type.
- String_from_single_item_array_with_null, String_from_typed_value_with_null,
  String_from_Property_with_null_value and String_from_VertexProperty_with_null_value:
  unwrapping a null does not make it a string.
- Property_of_string_from_null and VertexProperty_of_string_from_null: nor is it the
  value of a property that a scalar is wrapped into.
- Strings_from_Array_with_null: in an array it survives as a null, as Nullable_null
  has it for an int?.
- Constructor_arguments_from_null, Member_from_null and
  Language_with_null_property_value: in an object, a constructor argument is passed
  the null and a member is left as it was. A snapshot leaves out a member that is
  null and writes one that is empty, so these tell the two apart.

Tests only: this implementation already answers all thirteen. Measured against the
other one before its fix, it answered ten of them with the empty string in place of
the null - all but Uri_from_null, Person_from_null and Strings_from_Array_with_null.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.64%. Comparing base (2bd4a5a) to head (14da617).

Additional details and impacted files
@@            Coverage Diff             @@
##             14.x    #2477      +/-   ##
==========================================
+ Coverage   93.59%   93.64%   +0.05%     
==========================================
  Files         279      279              
  Lines        7903     7903              
  Branches      885      885              
==========================================
+ Hits         7397     7401       +4     
+ Misses        313      311       -2     
+ Partials      193      191       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@danielcweber
danielcweber merged commit 14da617 into 14.x Sep 30, 2026
6 checks passed
@danielcweber
danielcweber deleted the fix/string-from-null-contract branch September 30, 2026 22:29
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.

1 participant