@@ -49,8 +49,13 @@ class type_set {
4949 static type_set none () { return type_set{}; }
5050
5151 void set (json_type t) { bits_.set (index (t)); }
52+ void intersect (const type_set& other) { bits_ &= other.bits_ ; }
5253 bool test (json_type t) const { return bits_.test (index (t)); }
5354
55+ bool is_null_only () const {
56+ return bits_.count () == 1 && bits_.test (index (json_type::null));
57+ }
58+
5459 // / Returns the single non-null type if exactly one is set.
5560 std::optional<json_type> single_non_null_type () const {
5661 auto copy = bits_;
@@ -113,12 +118,56 @@ struct constraint {
113118
114119 // Object properties, keyed by name.
115120 std::map<std::string, constraint> properties = {};
121+ bool additional_properties_allowed = true ;
116122
117123 // Array item constraints.
118124 // - nullopt: no "items" keyword
119125 // - empty vector: "items": []
120126 // - non-empty: item schema(s) to validate
121127 std::optional<std::vector<constraint>> items = std::nullopt ;
128+
129+ conversion_outcome<void > intersect (const constraint& other) {
130+ types.intersect (other.types );
131+
132+ if (format.has_value () && other.format .has_value ()) {
133+ if (format != other.format ) {
134+ return conversion_exception (
135+ " Conflicting format annotations across branches" );
136+ }
137+ } else if (other.format .has_value ()) {
138+ format = other.format ;
139+ }
140+
141+ if (!properties.empty () && !other.properties .empty ()) {
142+ return conversion_exception (
143+ " Intersecting constraints with properties on both sides "
144+ " is not supported" );
145+ } else if (!other.properties .empty ()) {
146+ if (!additional_properties_allowed) {
147+ return conversion_exception (
148+ " additionalProperties: false conflicts with properties "
149+ " defined in another branch" );
150+ }
151+ properties = other.properties ;
152+ } else if (
153+ !properties.empty () && !other.additional_properties_allowed ) {
154+ return conversion_exception (
155+ " additionalProperties: false conflicts with properties "
156+ " defined in another branch" );
157+ }
158+ additional_properties_allowed = additional_properties_allowed
159+ && other.additional_properties_allowed ;
160+
161+ if (items.has_value () && other.items .has_value ()) {
162+ return conversion_exception (
163+ " Intersecting constraints with items on both sides is not "
164+ " supported" );
165+ } else if (other.items .has_value ()) {
166+ items = other.items ;
167+ }
168+
169+ return outcome::success ();
170+ }
122171};
123172
124173// / Context for the resolution phase.
@@ -149,6 +198,48 @@ collect(collect_context& ctx, const conversion::json_schema::subschema&);
149198
150199conversion_outcome<field_type> resolve (resolution_context&, const constraint&);
151200
201+ // This is not full fidelity oneOf support - only T|null is supported.
202+ // In the general case, oneOf is a XOR over multiple schemas. For T|null
203+ // it is reduced to OR.
204+ conversion_outcome<constraint> collect_one_of_t_xor_null (
205+ collect_context& ctx,
206+ const conversion::json_schema::const_list_view& one_of) {
207+ constexpr std::string_view unsupported_one_of_msg
208+ = " oneOf keyword is supported only for exclusive T|null structures" ;
209+ if (one_of.size () != 2 ) {
210+ return conversion_exception (std::string{unsupported_one_of_msg});
211+ }
212+
213+ auto c1 = collect (ctx, one_of.at (0 ));
214+ if (c1.has_error ()) {
215+ return c1.error ();
216+ }
217+ auto c2 = collect (ctx, one_of.at (1 ));
218+ if (c2.has_error ()) {
219+ return c2.error ();
220+ }
221+
222+ // Accept only exclusive oneOf(T, null):
223+ // - exactly one branch is null-only
224+ // - the non-null branch must not include null
225+ const bool c1_is_null_only = c1.value ().types .is_null_only ();
226+ const bool c2_is_null_only = c2.value ().types .is_null_only ();
227+
228+ if (c1_is_null_only == c2_is_null_only) {
229+ return conversion_exception (std::string{unsupported_one_of_msg});
230+ }
231+
232+ const constraint& non_null_branch = c1_is_null_only ? c2.value ()
233+ : c1.value ();
234+ if (non_null_branch.types .test (json_type::null)) {
235+ return conversion_exception (std::string{unsupported_one_of_msg});
236+ }
237+
238+ auto c = non_null_branch;
239+ c.types .set (json_type::null);
240+ return c;
241+ }
242+
152243// / Collect item constraints from a JSON Schema array.
153244conversion_outcome<std::optional<std::vector<constraint>>> collect_items (
154245 collect_context& ctx, const conversion::json_schema::subschema& s) {
@@ -232,12 +323,14 @@ collect(collect_context& ctx, const conversion::json_schema::subschema& s) {
232323 c.properties [name] = std::move (prop_constraint.value ());
233324 }
234325
235- if (
236- s.additional_properties ()
237- && s.additional_properties ()->get ().boolean_subschema () != false ) {
238- return conversion_exception (
239- " Only 'false' subschema is supported "
240- " for additionalProperties keyword" );
326+ if (s.additional_properties ()) {
327+ if (s.additional_properties ()->get ().boolean_subschema () == false ) {
328+ c.additional_properties_allowed = false ;
329+ } else {
330+ return conversion_exception (
331+ " Only 'false' subschema is supported "
332+ " for additionalProperties keyword" );
333+ }
241334 }
242335 }
243336
@@ -250,6 +343,17 @@ collect(collect_context& ctx, const conversion::json_schema::subschema& s) {
250343 c.items = std::move (items_result.value ());
251344 }
252345
346+ if (!s.one_of ().empty ()) {
347+ auto one_of_constraint = collect_one_of_t_xor_null (ctx, s.one_of ());
348+ if (one_of_constraint.has_error ()) {
349+ return one_of_constraint.error ();
350+ }
351+ auto intersect_result = c.intersect (one_of_constraint.value ());
352+ if (intersect_result.has_error ()) {
353+ return intersect_result.error ();
354+ }
355+ }
356+
253357 return c;
254358}
255359
0 commit comments