Skip to content

Commit c2cbcc3

Browse files
authored
Merge pull request #2 from barbacane-dev/fix/issue-954-type-union-with-subschemas
fix: preserve multi-type `type` union when schema has subschemas (oxidecomputer#954)
2 parents 30fb387 + 91580aa commit c2cbcc3

3 files changed

Lines changed: 1029 additions & 3 deletions

File tree

typify-impl/src/convert.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,11 +464,15 @@ impl TypeSpace {
464464
extensions: _,
465465
} => self.convert_unknown_enum(type_name, original_schema, metadata, enum_values),
466466

467-
// Subschemas
467+
// Subschemas with no body validation and either no type or a
468+
// single type. A multi-type (`Vec`) instance_type is deliberately
469+
// excluded so that it flows through the merge arm below, which
470+
// folds the type union into each subschema branch. Preserving the
471+
// earlier behaviour for `None` / `Single` keeps existing tolerant
472+
// handling of schemas whose outer type may conflict with a branch.
468473
SchemaObject {
469474
metadata,
470-
// TODO we probably shouldn't ignore this...
471-
instance_type: _,
475+
instance_type: None | Some(SingleOrVec::Single(_)),
472476
format: None,
473477
enum_values: None,
474478
const_value: None,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$comment": "Regression coverage for issue #954: schemas with a multi-type `type: [...]` array on the same object as `oneOf` / `anyOf` / `allOf` / `not` previously discarded the `type` constraint (TODO at convert.rs wildcarded `instance_type`). Single-type + subschema cases must still pass through the earlier arm unchanged.",
4+
"definitions": {
5+
"TypeArrayOneOfItems": {
6+
"$comment": "Canonical issue #954 shape. Each oneOf branch only constrains `items`, so the type union must be folded into every branch rather than dropped.",
7+
"type": [ "string", "number", "boolean", "array" ],
8+
"oneOf": [
9+
{ "items": { "type": "string" } },
10+
{ "items": { "type": "number" } },
11+
{ "items": { "type": "boolean" } }
12+
]
13+
},
14+
"TypeArrayAnyOfItems": {
15+
"$comment": "Same shape as TypeArrayOneOfItems but using anyOf. anyOf travels through try_merge_with_each_subschema on a sibling path from oneOf; it should fold the type union the same way.",
16+
"type": [ "string", "number", "array" ],
17+
"anyOf": [
18+
{ "items": { "type": "string" } },
19+
{ "items": { "type": "number" } }
20+
]
21+
},
22+
"TypeArrayAllOfRefinement": {
23+
"$comment": "allOf is folded pairwise into the parent rather than producing branches. The type union must survive and the array-only constraints should apply when the Array variant is selected.",
24+
"type": [ "string", "array" ],
25+
"allOf": [
26+
{ "items": { "type": "string" } },
27+
{ "minItems": 1 }
28+
]
29+
},
30+
"TypeArrayNotExclusion": {
31+
"$comment": "not: object is redundant when the outer type union excludes object, but merging must not drop the type union when the not branch is applied.",
32+
"type": [ "string", "number", "array" ],
33+
"not": { "type": "object" }
34+
},
35+
"SingleTypeOneOfArrayBranch": {
36+
"$comment": "Regression guard (rust-collisions pattern). Outer singleton type + oneOf where one branch has a conflicting explicit type. This must continue to pass through the earlier arm (no merge), otherwise the array branch becomes unsatisfiable and is silently dropped.",
37+
"type": "object",
38+
"oneOf": [
39+
{
40+
"type": "object",
41+
"properties": { "kind": { "type": "string" } },
42+
"required": [ "kind" ]
43+
},
44+
{
45+
"type": "array",
46+
"items": { "type": "string" },
47+
"minItems": 2,
48+
"maxItems": 2
49+
}
50+
]
51+
},
52+
"TypeArrayOneOfExplicitArrayBranches": {
53+
"$comment": "Case 7: each oneOf branch pins `type: array`, so the intersection with the outer type union must prune the non-array primitives. Only array variants should be emitted.",
54+
"type": [ "string", "array" ],
55+
"oneOf": [
56+
{ "type": "array", "items": { "type": "string" } },
57+
{ "type": "array", "items": { "type": "number" } }
58+
]
59+
},
60+
"TypeArrayPartiallyUnsatisfiableOneOf": {
61+
"$comment": "Some oneOf branches conflict with the outer type union and should be dropped during merge; the surviving branch must carry the outer type union. The two eliminated branches use object/number which the outer `[string, array]` disallows.",
62+
"type": [ "string", "array" ],
63+
"oneOf": [
64+
{ "type": "object", "properties": { "name": { "type": "string" } } },
65+
{ "items": { "type": "string" } },
66+
{ "type": "number" }
67+
]
68+
},
69+
"TypeArrayFullyUnsatisfiableOneOf": {
70+
"$comment": "Case 9: every branch conflicts with the outer type union, so `try_merge_with_each_subschema` returns empty and the schema resolves to never. Must emit an empty enum cleanly rather than panic.",
71+
"type": [ "string", "number" ],
72+
"oneOf": [
73+
{ "type": "array", "items": { "type": "string" } },
74+
{ "type": "object", "properties": { "k": { "type": "string" } } }
75+
]
76+
},
77+
"TypeArrayOneOfAndAllOf": {
78+
"$comment": "Case 10: oneOf and allOf on the same object, both alongside a multi-type `type` array. Exercises the full merge path (allOf folded first, then oneOf fanned out) with the Vec instance_type flowing through the merge arm.",
79+
"type": [ "string", "array" ],
80+
"allOf": [
81+
{ "minLength": 1 }
82+
],
83+
"oneOf": [
84+
{ "items": { "type": "string" } },
85+
{ "items": { "type": "number" } }
86+
]
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)