Skip to content

Report all missing members in a response, not just the first - #458

Merged
danielabbatt merged 4 commits into
mainfrom
claude/report-all-missing-members-0ffe0f
Oct 29, 2026
Merged

danielabbatt merged 4 commits into
mainfrom
claude/report-all-missing-members-0ffe0f

Conversation

@danielabbatt

Copy link
Copy Markdown
Contributor

Closes #457.

Summary

Under JsonMissingMemberHandling.LogWarningOnErrorAndContinue and ThrowOnError, the response is now deserialized once with MissingMemberHandling.Error and a per-call JsonSerializer.Error handler. Each missing-member error is recorded and marked handled, so Newtonsoft skips that member's value and carries on binding the rest of the object and later array items. A small JsonTextReader subclass records the property names inside a skipped value, so members nested inside a missing member are reported too. Every other error is left unhandled and propagates exactly as before.

  • New MerakiClientOptions.JsonMissingMembersAction (Action<Type, IReadOnlyList<JsonMissingMember>, string>), called once per affected response with every missing member in document order.
  • New JsonMissingMember (DeclaringType, MemberName, Path). For a member nested in a missing member, DeclaringType is the modelled type that lacks the outermost one, and Path shows the nesting (unknownObject.deeper.leaf).
  • New JsonMissingMembersException : JsonSerializationException with MissingMembers, thrown by ThrowOnError.
  • JsonMissingMemberAction kept and documented as superseded.

What counts as a missing member

A JsonSerializationException whose message starts with Could not find member '. Newtonsoft raises exactly that (unlocalized) text, "Could not find member '{name}' on object of type '{type}'. Path '{path}', line 1, position 2.", wherever MissingMemberHandling.Error rejects a member, and nowhere else. Anything else (JsonReaderException, "Error converting value", additional content, etc.) is not handled and throws as before.

Behaviour, old vs new

Ignore LogWarningOnErrorAndContinue old LogWarningOnErrorAndContinue new ThrowOnError old ThrowOnError new
Members found per response none reported first only all, including nested and on later array items first only all
JsonMissingMemberAction never once, first member's exception once, the same first member's exception once, first member's exception once, the same first member's exception
JsonMissingMembersAction never (did not exist) once, full list (did not exist) once, full list, before the throw
Warning logged none one, first member's message one, first member's message plus "N further missing member(s): 'path' on Type" followed by the rest none none
Response JSON logged at JsonMissingMemberResponseLogLevel no once once once once
Thrown nothing nothing nothing Newtonsoft JsonSerializationException for the first member JsonMissingMembersException (a JsonSerializationException), InnerException = the old exception, same Path/line/position; message identical when there is one member, otherwise the first message plus the others' paths
Deserialization passes, no missing members 1 1 1 1 1
Deserialization passes, with missing members 1 2 (strict, then Ignore) 1 1 (stops at first) 1 (reads to the end)
Returned object all declared members bound bound by the Ignore pass the same, from the single pass none none

Other errors:

old new
Type conversion / other JsonSerializationException, no missing member before it warning (LogWarning mode), JSON logged, JsonMissingMemberAction called with it, rethrown identical (LogWarning mode no longer runs a second pass just to rethrow it)
Such an error after one or more missing members LogWarning: action with the first missing member, then the Ignore pass throws the conversion error. ThrowOnError: the missing-member exception is thrown, the conversion error is never reached both modes: the missing members found so far are reported (warning in LogWarning mode, both actions), then the conversion error is thrown
JsonReaderException (invalid JSON, unparseable number) warning logged, rethrown identical; any missing members found before it are reported first

What existing consumers need to change

Nothing. JsonMissingMemberAction is still called once per affected response with an exception whose message, Path and type are the same as before. Code that catches JsonSerializationException from ThrowOnError still catches the new exception. To get every member, set JsonMissingMembersAction.

The one observable difference for ThrowOnError: when a response has missing members and a later conversion error, the conversion error is now what is thrown (after the missing members are reported), where before the missing-member exception was.

Is the partially built object correct?

Yes. Marking the error handled makes Newtonsoft skip only the offending member's value and continue with the next property, so every other member of that object, later array items and later top-level members still bind. MissingMembersReportingTests.LogWarning_HandledMissingMembers_LeaveTheRestOfTheObjectBound checks a response with unknowns at the top level, on a nested object, as a nested object/array and on the middle array item, and asserts the result equals the Ignore result. A mutation run (not marking the error handled) fails 10 of the 19 tests.

Tests

Meraki.Api.Test.Client.MissingMembersReportingTests, 19 tests, synthetic models and JSON:

  • several unknown members, including ones nested inside an unknown member and one on a later array item, reported with paths in a single JsonMissingMembersAction call
  • unknown member on a later item of a root-level array
  • object fully bound after handled errors (equal to the Ignore result)
  • legacy action called once with the same exception message and path Newtonsoft gives
  • one warning naming every member
  • no unknown members: no calls, no logs, root constructed once (both modes)
  • unknown members: root still constructed once (single pass)
  • ThrowOnError: both actions called, then one JsonMissingMembersException with the full list; single-member message unchanged
  • Ignore unchanged
  • type conversion error, reader error and invalid JSON still throw as before (both modes), with and without missing members before them

Offline test run (Meraki.Api.Test.exe -trait- Category=Integration): before 504 passed, after 523 passed (504 + 19 new), 0 failed.

Benchmark

Benchmarks/MissingMemberDeserialization.cs, 200-item synthetic array, LogWarningOnErrorAndContinue, --job short:

Response Before After Ratio Allocated before Allocated after
No missing members 153.5 us 159.2 us 1.04 202.3 KB 202.6 KB
4 items with a missing object member (2 nested members each) 210.1 us 163.2 us 0.78 236.2 KB 216.7 KB

The no-missing-members path is within short-run noise (one extra closure and a bool check per token); the affected path is about 22% faster because the second pass is gone, while now reporting 12 members instead of 1.

Not changed

MerakiMcpResult.Deserialize has its own ThrowOnError handling and still throws on the first missing member; it was out of scope here.

LogWarningOnErrorAndContinue and ThrowOnError now deserialize once with
MissingMemberHandling.Error and a per-call error handler that records each
missing member and marks it handled, so the rest of the response still binds.
Members nested inside a missing member are recorded while Newtonsoft skips
the value. Other errors still throw as before.

Adds MerakiClientOptions.JsonMissingMembersAction, JsonMissingMember and
JsonMissingMembersException. JsonMissingMemberAction is still called once
with the first member's exception and is documented as superseded.
LogWarningOnErrorAndContinue no longer deserializes affected responses twice.

Closes #457
@codacy-production

codacy-production Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 78 complexity

Metric Results
Complexity 78

View in Codacy

🟢 Coverage 85.29% diff coverage · +0.59% coverage variation

Metric Results
Coverage variation ✅ +0.59% coverage variation
Diff coverage ✅ 85.29% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (942e697) 7964 3061 38.44%
Head commit (ddfd69b) 8067 (+103) 3148 (+87) 39.02% (+0.59%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#458) 102 87 85.29%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

AI Reviewer: run a review on demand. To trigger the first review automatically, go to your organization or repository integration settings. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

Split the lazy list assignment from the Add call, compare the benchmark modulus with zero, and use or suppress the ILogger parameters the test logger must declare.
@danielabbatt
danielabbatt force-pushed the claude/report-all-missing-members-0ffe0f branch from 55d33a2 to d9f844e Compare October 26, 2026 10:15
…sing-members-0ffe0f

# Conflicts:
#	CHANGELOG.md
…sing-members-0ffe0f

# Conflicts:
#	CHANGELOG.md
@danielabbatt
danielabbatt merged commit e0cb834 into main Oct 29, 2026
8 checks passed
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.

Report all missing members in a response, not just the first

1 participant