diff --git a/include/glaze/json/schema.hpp b/include/glaze/json/schema.hpp index 95adbfd85f..560db8e4b2 100644 --- a/include/glaze/json/schema.hpp +++ b/include/glaze/json/schema.hpp @@ -143,6 +143,28 @@ namespace glz } }; + // Distinguish JSON Schemas generated for serialization output vs parsing input. + // + // output: schema describes what glaze will produce. All non-skipped object members + // are guaranteed to be written, and tuples always serialize at their full + // length, so the schema emits `required` and `minItems = maxItems`. + // + // input: schema describes what glaze will accept. Partial parsing is supported, + // so `required` is omitted on objects (except for variant discriminator + // tags, which must be present to select an alternative) and `minItems` + // is omitted on tuples. + enum struct schema_purpose : uint8_t { input, output }; + + consteval schema_purpose check_schema_purpose(auto&& Opts) + { + if constexpr (requires { Opts.schema_purpose_v; }) { + return Opts.schema_purpose_v; + } + else { + return schema_purpose::output; + } + } + namespace detail { enum struct defined_formats : uint32_t; @@ -907,6 +929,11 @@ namespace glz }); s.items = false; s.maxItems = N; + if constexpr (check_schema_purpose(Opts) == schema_purpose::output) { + // Output schemas describe serialized data: tuples always emit every element, + // so the array length is fixed (minItems == maxItems == N). + s.minItems = N; + } } }; @@ -973,6 +1000,8 @@ namespace glz using V = std::decay_t; + constexpr auto purpose = check_schema_purpose(Opts); + if constexpr (requires { meta::required; }) { if (!s.required) { s.required = std::vector{}; @@ -1000,8 +1029,12 @@ namespace glz using val_t = std::decay_t>; static constexpr sv key = reflect::keys[I]; - if constexpr (requires_key(key)) { - req.emplace_back(key); + // Output schemas describe guaranteed-present keys after serialization. + // Input schemas omit `required` so partial parsing is permitted. + if constexpr (purpose == schema_purpose::output) { + if constexpr (requires_key(key)) { + req.emplace_back(key); + } } schema prop{}; @@ -1290,12 +1323,22 @@ namespace glz bool write_type_info = false; }; - template + // Carries the schema_purpose tag through to_json_schema specializations via the existing + // `auto Opts` template parameter. Kept at namespace scope to avoid MSVC issues with + // function-local non-type template arguments. + template + struct opts_with_schema_purpose : std::decay_t + { + schema_purpose schema_purpose_v = schema_purpose::output; + }; + + template [[nodiscard]] error_ctx write_json_schema(Buffer&& buffer) { + static constexpr auto schema_opts = opts_with_schema_purpose{{Opts}, Purpose}; schema s{}; s.defs.emplace(); - detail::to_json_schema>::template op(s, *s.defs); + detail::to_json_schema>::template op(s, *s.defs); s.title = name_v; // Inline single-use $defs entries at their reference sites @@ -1314,17 +1357,44 @@ namespace glz return write(std::move(s), std::forward(buffer)); } - template + template [[nodiscard]] glz::expected write_json_schema() { std::string buffer{}; - const error_ctx ec = write_json_schema(buffer); + const error_ctx ec = write_json_schema(buffer); if (bool(ec)) [[unlikely]] { return glz::unexpected(ec); } return {buffer}; } + // Convenience entry points for the two schema purposes. Output schemas describe what + // glaze guarantees on serialization (required keys, fixed tuple lengths). Input schemas + // describe what glaze accepts on parsing (partial objects allowed, partial tuples allowed). + template + [[nodiscard]] error_ctx write_json_input_schema(Buffer&& buffer) + { + return write_json_schema(std::forward(buffer)); + } + + template + [[nodiscard]] glz::expected write_json_input_schema() + { + return write_json_schema(); + } + + template + [[nodiscard]] error_ctx write_json_output_schema(Buffer&& buffer) + { + return write_json_schema(std::forward(buffer)); + } + + template + [[nodiscard]] glz::expected write_json_output_schema() + { + return write_json_schema(); + } + // Custom reader for sv_opt: delegates to string_view reader, then assigns to sv_opt. template <> struct from diff --git a/tests/json_test/jsonschema_test.cpp b/tests/json_test/jsonschema_test.cpp index f1de935fb8..0463606f0a 100644 --- a/tests/json_test/jsonschema_test.cpp +++ b/tests/json_test/jsonschema_test.cpp @@ -573,7 +573,7 @@ suite value_type_variant_schema = [] { auto schema = glz::write_json_schema().value(); expect( schema == - R"({"type":"array","prefixItems":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"string"},{"type":"boolean"}],"items":false,"title":"std::tuple","maxItems":3})") + R"({"type":"array","prefixItems":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"string"},{"type":"boolean"}],"items":false,"title":"std::tuple","minItems":3,"maxItems":3})") << schema; }; @@ -1176,4 +1176,74 @@ suite schema_round_trip_test = [] { }; }; +namespace schema_purpose_fixtures +{ + struct partial_person + { + std::string name{}; + int age{}; + std::optional nickname{}; + }; +} + +suite schema_purpose_test = [] { + using namespace schema_purpose_fixtures; + + "input schema for tuple omits minItems"_test = [] { + using tuple_t = std::tuple; + auto s = glz::write_json_input_schema().value(); + glz::schema obj{}; + auto err = glz::read(obj, s); + expect(!err) << glz::format_error(err, s); + expect(obj.maxItems.has_value()); + expect(*obj.maxItems == 3UL); + expect(!obj.minItems.has_value()) << "input schema should not set minItems on a tuple: " << s; + }; + + "output schema for tuple sets minItems == maxItems"_test = [] { + using tuple_t = std::tuple; + auto s = glz::write_json_output_schema().value(); + glz::schema obj{}; + auto err = glz::read(obj, s); + expect(!err) << glz::format_error(err, s); + expect(obj.maxItems.has_value()); + expect(obj.minItems.has_value()) << "output schema should set minItems on a tuple: " << s; + expect(*obj.minItems == *obj.maxItems); + expect(*obj.minItems == 3UL); + }; + + "input schema for object omits required from non-optional members"_test = [] { + auto s = glz::write_json_input_schema() + .value(); + glz::schema obj{}; + auto err = glz::read(obj, s); + expect(!err) << glz::format_error(err, s); + expect(!obj.required.has_value()) + << "input schema must allow partial parsing - required must be absent: " << s; + }; + + "output schema for object emits required for non-optional members"_test = [] { + auto s = glz::write_json_output_schema() + .value(); + glz::schema obj{}; + auto err = glz::read(obj, s); + expect(!err) << glz::format_error(err, s); + expect(obj.required.has_value()) << "output schema must list required keys: " << s; + auto& req = *obj.required; + expect(std::find(req.begin(), req.end(), "name") != req.end()); + expect(std::find(req.begin(), req.end(), "age") != req.end()); + expect(std::find(req.begin(), req.end(), "nickname") == req.end()) + << "nullable members must not appear in required"; + }; + + "default write_json_schema matches output purpose"_test = [] { + using tuple_t = std::tuple; + auto def = glz::write_json_schema().value(); + auto out = glz::write_json_output_schema().value(); + expect(def == out) << "default purpose should be output: \n default: " << def << "\n output: " << out; + }; +}; + int main() { return 0; }