Skip to content

Read the JSON of a Newtonsoft contract test the way a response is read, and say what a date-like string is - #2478

Merged
danielcweber merged 3 commits into
14.xfrom
fix/contract-test-date-parsing
Sep 30, 2026
Merged

danielcweber merged 3 commits into
14.xfrom
fix/contract-test-date-parsing

Conversation

@danielcweber

@danielcweber danielcweber commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

The Newtonsoft contract tests now read their JSON the way a server response is read, with DateParseHandling.None, so that a string that looks like a date reaches Gremlinq's converters as the string it is. Until now they used JToken.Parse, which turns such a string into a date before any converter has seen it, in the machine's time zone - a path no response takes, and one on which both deserializers could not be held to the same answer. With that gone, the contract says what a date-like string is read as: a date where a date is asked for, and the string it came as everywhere else.

Changes

  • Support.NewtonsoftJson.Tests: GraphsonSupportTest.CreateNativeToken mirrors DeferToNewtonsoftConverterFactory: UTF-8 bytes through a StreamReader and a JsonTextReader with DateParseHandling.None, everything else on the reader as it is created (FloatParseHandling.Double, a MaxDepth of 64, the invariant culture), and the token built by a default JsonSerializer rather than by JToken.Load. The last part is mirrored too, because it is not the same: a name that occurs twice is kept at its last place rather than its first, a comment is kept as a token, and text after the token is not an error.
  • Support.NewtonsoftJson.Tests: four tests in TransformerTest hand the deserializer a JValue that holds a DateTime or a DateTimeOffset. No token of a response holds a date, so with the contract tests reading as a response is read, the arms of DateTimeConverterFactory and DateTimeOffsetConverterFactory that take such a token were reached by nothing - yet a token a caller read or built on its own may hold one. The date it holds is the date that is read, as it is held: a DateTime keeps its kind, a DateTimeOffset its offset.
  • Tests.Infrastructure: fifteen contract tests after DateTimeOffset_from_invalid_string, each answered alike by Newtonsoft and System.Text.Json:
    • a string naming an offset, as a DateTimeOffset and as a DateTime: the instant it names, in UTC. A fraction of a second is kept down to the tick. DateTimeOffset_from_double is the counterpart DateTime_from_double did not have.
    • a string naming no offset, "2020-01-02", as a DateTime and as a DateTimeOffset: the local midnight of the machine reading it. The instant differs from machine to machine, so these two hold that it is that local midnight rather than the instant.
    • a date-like string as a string and as an object - naming UTC, naming an offset, a date alone, in an array read as object[] and as a value of a g:Map: the string it came as.
    • a date-like string as a property of an entity: Person.RegistrationDate reads it as the date, Person.Name keeps it as the string, and a RegistrationDate that is no date costs the property, not the person.

Notes

  • Tests only; nothing in src/ changes, hence skip-changelog.
  • No existing snapshot changed with the way of reading: the three date-like strings the contract had all name UTC and are all asked for as dates. Measured in Europe/Berlin for what it had not: "2020-01-02T03:04:05+02:00" as a DateTimeOffset was 02:04:05 +01:00 with JToken.Parse and is 01:04:05 +00:00 as a response is read; as an object it was a DateTime and is the string; as a string it was 01/02/2020 02:04:05 and is the string as it came.
  • The Newtonsoft tests went from 299 to 318 and pass in Europe/Berlin, America/New_York and Asia/Kolkata alike. The System.Text.Json contract tests in Gremlinq.Extensions verify against the same snapshots and pass with them unchanged, in the same three time zones.
  • Coverage: the first run of this pull request had codecov/project fail at 93.55% (-0.04%), three lines in the two date converters named above and nowhere else. The four TransformerTest tests are the answer to that; the arm of DateTimeOffsetConverterFactory for a token holding a DateTimeOffset had been covered by nothing before either.
  • The other tests of Support.NewtonsoftJson.Tests that parse with JObject.Parse (TransformerTest, JTokenHeuristicTests, DynamicDictionaryTests) are left as they are: none of their literals has a date-like string, a repeated name or a comment, so both ways of reading give them the same token. Issue219 already goes through the reader a response goes through.
  • Left out, because the two implementations do not answer alike, and reported rather than changed here: the offset of a DateTimeOffset read from a string naming none (UTC on Newtonsoft, the machine's own on System.Text.Json, the instant being the same), "/Date(1577934245000)/" (a date on Newtonsoft through its own fallback, declined by System.Text.Json), and dates at the very ends of the range.
  • The Gremlinq.Extensions forward rides along with the next pull request there and needs no change of its own.

🤖 Generated with Claude Code

@danielcweber danielcweber added the skip-changelog Excluded from release notes and description check label Sep 30, 2026
@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.67%. Comparing base (14da617) to head (7e82345).

Additional details and impacted files
@@            Coverage Diff             @@
##             14.x    #2478      +/-   ##
==========================================
+ Coverage   93.64%   93.67%   +0.02%     
==========================================
  Files         279      279              
  Lines        7903     7903              
  Branches      885      885              
==========================================
+ Hits         7401     7403       +2     
+ Misses        311      310       -1     
+ Partials      191      190       -1     

☔ 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 and others added 3 commits October 1, 2026 00:42
GraphsonSupportTest turned the JSON of a contract test into a token with
JToken.Parse. A response is not read that way: DeferToNewtonsoftConverterFactory
sets DateParseHandling.None on its reader, so that a string stays a string and
Gremlinq's own converters decide whether it is a date. JToken.Parse leaves the
default, DateParseHandling.DateTime, under which a string that looks like a date is
a Date token before any converter has seen it. For such strings the contract tests
took a path no response ever takes, and answered differently, depending on the
machine's time zone. Measured in Europe/Berlin, with JToken.Parse and as a response
is read:

- "2020-01-02T03:04:05+02:00" as a DateTimeOffset: 02:04:05 +01:00, against
  01:04:05 +00:00. As a DateTime: 02:04:05 Local, against 01:04:05 Utc.
- The same string as an object: a DateTime, against the string. As a string:
  "01/02/2020 02:04:05", against the string as it came.
- "2020-01-02T03:04:05Z" as an object: a DateTime, against the string.

CreateNativeToken now reads as DeferToNewtonsoftConverterFactory does: UTF-8 bytes
through a StreamReader and a JsonTextReader with DateParseHandling.None, everything
else on the reader as it is created, and the token built by a default
JsonSerializer. That last part differs from JToken.Load in ways no test depends on
today, and is mirrored so that none comes to depend on the difference: a name that
occurs twice is kept at its last place rather than its first, a comment is kept as
a token, and text after the token is not an error.

No snapshot changes: no existing contract test has a string that looks like a date
and would be read as anything but the UTC date it names. All 299 tests pass as
before.

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

The contract tests had three strings that look like a date, all naming UTC and all
asked for as a date. What a string naming an offset is read as, what one naming
none is read as, and what either is when no date is asked for, was said nowhere -
and until the Newtonsoft tests read their JSON the way a response is read, it could
not have been said: they answered with what Newtonsoft's own date parsing made of
the string, in the machine's time zone.

Fifteen tests, after DateTimeOffset_from_invalid_string, each answered alike by
both implementations:

- A string naming an offset is read as the instant it names and handed back in
  UTC: "2020-01-02T03:04:05+02:00" is 01:04:05 +0 as a DateTimeOffset and 01:04:05
  Utc as a DateTime. A fraction of a second is kept down to the tick.
  DateTimeOffset_from_double is the counterpart DateTime_from_double did not have.
- A string naming no offset, "2020-01-02", is the local midnight of the machine
  reading it. The instant differs from machine to machine, so these two tests hold
  that it is that local midnight instead of the instant. The DateTimeOffset is
  compared by its instant alone: its offset is UTC on Newtonsoft and the machine's
  own on System.Text.Json, wherever the machine is not in UTC.
- Asked for as a string or as an object, the string is the string it came as: on
  its own with UTC, with an offset and as a date alone, in an array read as
  object[], and as a value of a g:Map read as an object.
- As a property of an entity it is what the property says. Person.RegistrationDate
  reads it as the date, Person.Name keeps it as the string, and a RegistrationDate
  that is no date costs the property, not the person.

The Newtonsoft tests, 314 now, pass in Europe/Berlin, America/New_York and
Asia/Kolkata alike, and so do these on System.Text.Json.

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

Read as a response is read, no token of a contract test holds a date any more, so
the arms of DateTimeConverterFactory and DateTimeOffsetConverterFactory that take
a JValue holding a DateTime were reached by nothing: Codecov counted three lines
less on the pull request, in these two files alone. The arms are not dead. A token
that a caller read or built on its own may hold a date, and the deserializer is
handed it all the same.

Four tests in TransformerTest hand in such a token:

- A DateTime from a token holding a DateTime is that DateTime as it is held, its
  kind included: one of unspecified kind stays unspecified and is not moved.
- A DateTime from a token holding a DateTimeOffset is the instant in UTC.
- A DateTimeOffset from a token holding a DateTime is that instant.
- A DateTimeOffset from a token holding a DateTimeOffset keeps its offset. This
  arm was covered by nothing before either.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@danielcweber
danielcweber force-pushed the fix/contract-test-date-parsing branch from 489b6be to 7e82345 Compare September 30, 2026 22:46
@danielcweber
danielcweber merged commit 7e82345 into 14.x Sep 30, 2026
5 checks passed
@danielcweber
danielcweber deleted the fix/contract-test-date-parsing branch September 30, 2026 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Excluded from release notes and description check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant