Repository navigation
Expose the location of each parse failure via rfl::Error::details() - #766
clockblocker wants to merge 2 commits into
Conversation
rfl::Error keeps its message, but now also carries the individual failures it summarizes, each with the path of field names and array indices at which it occurred. The readers that prefix nested messages with "Failed to parse field ..." record a path segment at the same place, so programs no longer need to parse what() to find out which field failed. Closes munich-data-engineering#765. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A note on why
If you'd prefer |
|
|
@clockblocker the PR looks great. Thanks for the initiative. I have minor comments (see above). Also, the build fails on at least some of the compiler settings. Could you take a look? |
Clang 17 does not support floating-point non-type template arguments, so rfl::Minimum<0.0> fails to compile in the JSON-C++20-llvm-17 job. An integer bound tests the same thing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thank you for the review. Don't see any "Minor comments" beyond the failing build, though. Ai-authored: Build failure: fixed in 0cc1fcd. The new test used |
| } | ||
| }; | ||
|
|
||
| using DetailsPtr = std::unique_ptr<std::vector<ErrorDetail>, DeleteDetails>; |
There was a problem hiding this comment.
@clockblocker Could you use rfl::Box here? Clearly, the implicit assumption here is that DetailsPtr should not be NULL, so rfl::Box establishes that more clearly.
Moreover, if you use rfl::Box, you won't have to implement the copy operations by hand.
| using DetailsPtr = std::unique_ptr<std::vector<ErrorDetail>, DeleteDetails>; | ||
|
|
||
| RFL_COLD static DetailsPtr copy_details(const DetailsPtr& _ptr) { | ||
| return _ptr ? DetailsPtr(new std::vector<ErrorDetail>(*_ptr)) : nullptr; |
There was a problem hiding this comment.
This is what I mean...rfl::Box can handle that more elegantly.
|
@clockblocker you should see the minor comments now |
Why
I'm using the library in the production project to establish a source of truth for the chain:
Part of the app is a complex form with tricky (yet expressible in Standard Schema) validation rules.
Would like to express the {message, path} in the source-of-truth struct, so that all the clients can expect to send messages like:
and receive errors like:
AI-authored
Context
TypeScript validation libraries such as Zod, Valibot, ArkType and Effect Schema report failures in the Standard Schema shape: a list of issues, each with a
messageand apath. A C++ backend that validates with reflect-cpp can't produce that list today.what()is a single prose string, so a client can't tell which form field to highlight unless it parses the message text.details()maps directly onto Standard Schema issues. EachErrorDetailbecomes one issue, field names become string keys, and indices become numbers. A web client can then show backend validation errors the same way it shows its own.Summary
Closes #765.
rfl::Errorkeepswhat()exactly as it is, and addsdetails(): one entry per failure, each with the path where it occurred.An error that was built from a message alone reports itself as one detail with an empty path. Errors from custom parsers and validators therefore need no changes.
Each reader adds a path segment at the point where it already prefixes the message:
ViewReader / MapReader "Failed to parse field 'x': ..." + segment "x" NamedTupleParser "Field named 'x' not found." + segment "x" ViewReader (NoExtraFields) "Value named 'x' not used." + segment "x" ArrayReader / TupleReader "Failed to parse element/field i" + segment i +VectorReader (message unchanged) + segment i ParserTaggedUnion prefix only; inner paths are kept to_single_error (new) joins messages as before, concatenates detailsUntagged variants stay a single failure at the variant's location, because reflect-cpp can't tell which alternative the input was meant to be. The joined
what()still shows at most 10 errors, butdetails()contains all of them.FieldErrorsandMapReadernow collectErrors instead of strings. The new helpers live inparsing/wrap_error.hpp. Docs are indocs/content/result.mdunder "Locating errors".Evidence
The issue's example, via
rfl::cli::read(tests/cli/test_errors.cpp):The index
1inretriesexists only indetails(). Today'swhat()doesn't say which element failed.tests/json/test_error_details.cppcovers nested structs in vectors withSnakeCaseToCamelCase, maps,std::array, tuples, sets with duplicates,NoExtraFields,FailFast,DefaultIfMissing, tagged unions, untagged variants, and root-level errors. Existing tests that comparewhat()exactly still pass, which shows the messages are unchanged.Local runs (Apple clang 17,
REFLECTCPP_USE_VCPKG=OFF): JSON 273, CLI 72, generic 98, env 41, json_c_arrays_and_inheritance 7, all passing. The new tests plustest_error_messages,test_fail_fastandtest_tagged_unionalso pass withREFLECTCPP_USE_STD_EXPECTED/ C++23. I couldn't build the other formats locally, so CI will be their first run. They share the changed parsing code, and no format-specific code uses the changed internals.Performance. Benchmark: read a
std::vectorof 100k nested structs from JSON, 40 runs, median of interleaved rounds.My first version stored the details as a plain
std::vectormember and cost +8.5%. Bisecting showed the cause:~Error()is inlined into everyResult<T>destructor, and~vector<ErrorDetail>is a lot of code. So the details now sit behind aunique_ptrwhose deleter and copy are out of line, using a newRFL_COLDmacro (noinline+cold). The error-wrapping helpers use it too.sizeof(rfl::Error)grows by 8 bytes (24 → 32 on libc++). The vector index comes fromsize()instead of a per-element counter; maps and sets keep a counter because duplicates don't growsize().Merge Danger
Door: two-way. The API is purely additive and nothing is persisted.
Blast Radius: small. Two things could affect downstream code:
sizeof(rfl::Error)or on its copy being trivially defaulted (unlikely).rfl::parsing::MapReaderdirectly or depends onFieldErrorsbeingstd::vector<std::string>. Both are internal.Not included: the optional "kind" field (missing / extra / validation / wrong type) from the issue. I can add it here or in a follow-up if you want it.
🤖 Generated with Claude Code