From 127ec54a1c12359062cacb866f23ef17403b70de Mon Sep 17 00:00:00 2001 From: clockblocker Date: Wed, 11 Nov 2026 08:04:39 +0100 Subject: [PATCH] Say why default construction failed when reading a struct with defaults A struct that contains rfl::DefaultVal or is read with rfl::DefaultIfMissing is default-constructed before it is read. If T{} throws, for instance because a validated field has no valid default value, the message of the exception was returned as it is: Size validation failed: Value expected to be greater than or equal to 1, but got 0. This reads as if the input was wrong, even when the field is in the input and valid, and it names neither the struct nor the cause. What is read and what fails stays the same. But the error now names the struct, says why it was default-constructed and what to do about it, followed by the original message: Could not default-construct Person, which is required because it contains rfl::DefaultVal. Give every field a valid default value. Original error: Size validation failed: Value expected to be greater than or equal to 1, but got 0. Only a failure of T{} gets this text. An exception thrown later in read_struct_with_default keeps its own message. Also document the restriction and the workaround in default_val.md and processors.md: a field that is given an initializer for this reason stays required, unless the struct is read with rfl::DefaultIfMissing. Co-Authored-By: Claude Fable 5.2 --- docs/content/default_val.md | 36 +++++ docs/content/processors.md | 4 + include/rfl/parsing/Parser_default.hpp | 43 ++++-- tests/json/test_default_construct_error.cpp | 147 ++++++++++++++++++++ 4 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 tests/json/test_default_construct_error.cpp diff --git a/docs/content/default_val.md b/docs/content/default_val.md index ced77a072..6b29b52ba 100644 --- a/docs/content/default_val.md +++ b/docs/content/default_val.md @@ -52,6 +52,42 @@ const auto homer = rfl::json::read(R"(["Homer"])").va // homer.town == "" ``` +## All fields need a valid default value + +A struct that contains an `rfl::DefaultVal` is default-constructed before it is read. The fields found in the input are then written into it. This means that every field of the struct needs a valid default value, even a required field that is in the input. + +This matters when the default value of a field's type is not valid, for instance when the field has a validator: + +```cpp +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + rfl::Validator, rfl::Size>> favorite_foods; +}; + +// Fails, even though favorite_foods is in the input. +rfl::json::read(R"({"favorite_foods":["donuts","pizza"]})"); +``` + +An empty vector does not satisfy `rfl::Size>`, so `Person{}` throws an exception and you get the following error: + +``` +Could not default-construct Person, which is required because it contains rfl::DefaultVal. Give every field a valid default value. Original error: Size validation failed: Value expected to be greater than or equal to 1, but got 0. +``` + +To avoid this, give the field a valid default value: + +```cpp +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + rfl::Validator, rfl::Size>> favorite_foods = + std::vector({"none"}); +}; +``` + +The field is still required. If `favorite_foods` is missing in the input, you get the error `Field named 'favorite_foods' not found.` The default value is only used for a missing field if you pass the `rfl::DefaultIfMissing` processor. + +The same restriction applies to every struct that is read with the `rfl::DefaultIfMissing` processor, whether it contains an `rfl::DefaultVal` or not. + ## When to use Use `rfl::DefaultVal` when you want a field to be optional at the input side but still available as a value on the resulting object (no std::optional or pointer indirection). It is particularly useful for fields with sensible defaults (for example, a common last name, a default configuration value, or empty containers/strings). diff --git a/docs/content/processors.md b/docs/content/processors.md index 64c75dd52..a431bb4e6 100644 --- a/docs/content/processors.md +++ b/docs/content/processors.md @@ -310,6 +310,10 @@ have gotten had you read the following JSON string: Because you have not passed a default value to town, the default value of the type is used instead. +Note that the struct is default-constructed before it is read. Every field +therefore needs a valid default value, even if the field is in the input. +This is explained in the documentation on `rfl::DefaultVal`. + ### `rfl::DurationsAsCount` By default, `std::chrono::duration` types are written as an object containing diff --git a/include/rfl/parsing/Parser_default.hpp b/include/rfl/parsing/Parser_default.hpp index f17c13e16..91cd540d6 100644 --- a/include/rfl/parsing/Parser_default.hpp +++ b/include/rfl/parsing/Parser_default.hpp @@ -2,12 +2,14 @@ #define RFL_PARSING_PARSER_DEFAULT_HPP_ #include +#include #include #include "../Result.hpp" #include "../always_false.hpp" #include "../internal/HasFromReflectionMethod.hpp" #include "../internal/always_inline.hpp" +#include "../internal/cold.hpp" #include "../internal/default_if_missing_v.hpp" #include "../internal/has_default_val_v.hpp" #include "../internal/has_reflection_method_v.hpp" @@ -720,22 +722,39 @@ struct Parser { static Result read_struct_with_default(const R& _r, const InputVarType& _var) { try { - auto t = T{}; // This might fail, for instance if the default value does - // not satisfy the validator, but in that case we will just - // return the error. - auto view = ProcessorsType::template process(to_view(t)); - using ViewType = decltype(view); - auto err = - Parser::read_view_with_default( - _r, _var, &view); - if (err) [[unlikely]] { - return error(std::move(*err)); + // This might fail, for instance if the default value of a field does not + // satisfy its validator. The outer handler is for this line only. + auto t = T{}; + try { + auto view = ProcessorsType::template process(to_view(t)); + using ViewType = decltype(view); + auto err = + Parser::read_view_with_default( + _r, _var, &view); + if (err) [[unlikely]] { + return error(std::move(*err)); + } + return t; + } catch (std::exception& e) { + return error(e.what()); } - return t; } catch (std::exception& e) { - return error(e.what()); + return error(default_construction_error(e.what())); } } + + /// The error returned by read_struct_with_default when T{} throws. On its + /// own, the exception reads as if the input was wrong, so we name the struct + /// and explain why it was default-constructed. + RFL_COLD static Error default_construction_error(const char* _what) { + const std::string reason = internal::has_default_val_v + ? "it contains rfl::DefaultVal" + : "it is read with rfl::DefaultIfMissing"; + return Error( + "Could not default-construct " + type_name_t().str() + + ", which is required because " + reason + + ". Give every field a valid default value. Original error: " + _what); + } }; } // namespace rfl::parsing diff --git a/tests/json/test_default_construct_error.cpp b/tests/json/test_default_construct_error.cpp new file mode 100644 index 000000000..4157eadd9 --- /dev/null +++ b/tests/json/test_default_construct_error.cpp @@ -0,0 +1,147 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_default_construct_error { + +using Foods = + rfl::Validator, rfl::Size>>; + +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + Foods favorite_foods; +}; + +struct PersonWithValidDefault { + rfl::DefaultVal name = "Homer Simpson"; + Foods favorite_foods = std::vector({"none"}); +}; + +struct Pet { + std::string name; + Foods favorite_foods; +}; + +struct Family { + std::string town; + Person head; +}; + +inline int no_default_port() { throw std::runtime_error("No default port"); } + +struct Server { + rfl::DefaultVal host = "localhost"; + int port = no_default_port(); +}; + +bool contains(const std::string& _str, const std::string& _substr) { + return _str.find(_substr) != std::string::npos; +} + +const std::string validation_error = + "Size validation failed: Value expected to be greater than or equal to 1, " + "but got 0."; + +TEST(json, test_default_construct_error_with_default_val) { + const auto res = + rfl::json::read(R"({"favorite_foods":["donuts","pizza"]})"); + + ASSERT_FALSE(res && true); + const auto what = res.error().what(); + EXPECT_TRUE(contains(what, "default-construct")) << what; + EXPECT_TRUE(contains(what, "Person")) << what; + EXPECT_TRUE(contains(what, "rfl::DefaultVal")) << what; + EXPECT_TRUE(contains(what, "valid default value")) << what; + EXPECT_TRUE(contains(what, validation_error)) << what; +} + +TEST(json, test_default_construct_error_with_default_if_missing) { + const auto json_string = R"({"name":"Bart","favorite_foods":["pizza"]})"; + + EXPECT_TRUE(rfl::json::read(json_string) && true); + + const auto res = rfl::json::read(json_string); + + ASSERT_FALSE(res && true); + const auto what = res.error().what(); + EXPECT_TRUE(contains(what, "default-construct")) << what; + EXPECT_TRUE(contains(what, "Pet")) << what; + EXPECT_TRUE(contains(what, "rfl::DefaultIfMissing")) << what; + EXPECT_TRUE(contains(what, validation_error)) << what; +} + +TEST(json, test_default_construct_error_in_nested_struct) { + const auto res = rfl::json::read( + R"({"town":"Springfield","head":{"favorite_foods":["donuts"]}})"); + + ASSERT_FALSE(res && true); + const auto what = res.error().what(); + EXPECT_TRUE(contains(what, "Failed to parse field 'head': ")) << what; + + const auto details = res.error().details(); + ASSERT_EQ(details.size(), 1); + EXPECT_EQ(details[0].path, + std::vector({"head"})); + EXPECT_TRUE(contains(details[0].message, "default-construct")); + EXPECT_TRUE(contains(details[0].message, "Person")); + EXPECT_TRUE(contains(details[0].message, validation_error)); +} + +TEST(json, test_default_construct_error_without_validator) { + const auto res = rfl::json::read(R"({"port":8080})"); + + ASSERT_FALSE(res && true); + const auto what = res.error().what(); + EXPECT_TRUE(contains(what, "default-construct")) << what; + EXPECT_TRUE(contains(what, "Server")) << what; + EXPECT_TRUE(contains(what, "No default port")) << what; +} + +TEST(json, test_default_construct_error_valid_default) { + const auto homer = rfl::json::read( + R"({"favorite_foods":["donuts","pizza"]})") + .value(); + + EXPECT_EQ(homer.name.value(), "Homer Simpson"); + EXPECT_EQ(homer.favorite_foods.value(), + std::vector({"donuts", "pizza"})); + + // The initializer does not make the field optional. + const auto missing = rfl::json::read(R"({})"); + + ASSERT_FALSE(missing && true); + EXPECT_EQ(missing.error().what(), "Field named 'favorite_foods' not found."); + + // With rfl::DefaultIfMissing, the initializer is the value of a missing + // field. + const auto with_default = + rfl::json::read(R"({})") + .value(); + + EXPECT_EQ(with_default.favorite_foods.value(), + std::vector({"none"})); +} + +TEST(json, test_default_construct_error_not_used_for_later_failures) { + const auto wrong_type = + rfl::json::read(R"({"favorite_foods":"donuts"})"); + + ASSERT_FALSE(wrong_type && true); + EXPECT_TRUE(contains(wrong_type.error().what(), + "Failed to parse field 'favorite_foods': ")) + << wrong_type.error().what(); + EXPECT_FALSE(contains(wrong_type.error().what(), "default-construct")) + << wrong_type.error().what(); + + const auto invalid = + rfl::json::read(R"({"favorite_foods":[]})"); + + ASSERT_FALSE(invalid && true); + EXPECT_EQ(invalid.error().what(), + "Failed to parse field 'favorite_foods': " + validation_error); +} +} // namespace test_default_construct_error