Skip to content

fix allOf of oneOfs by introducing cartesian product#982

Open
PolyProgrammist wants to merge 3 commits intooxidecomputer:mainfrom
PolyProgrammist:allOfofOneOfs
Open

fix allOf of oneOfs by introducing cartesian product#982
PolyProgrammist wants to merge 3 commits intooxidecomputer:mainfrom
PolyProgrammist:allOfofOneOfs

Conversation

@PolyProgrammist
Copy link

A fix for #897

@PolyProgrammist
Copy link
Author

@ahl could you please review?

@PolyProgrammist
Copy link
Author

PolyProgrammist commented Feb 25, 2026

Please also see the PR #988 - with this, looks like there is no need for current PR.

Though this may be more relevant as it uses new merge logic as discussed here #176

Copy link
Collaborator

@ahl ahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems reasonable, and the improvement to the github output is encouraging.

new_variants: &[Schema],
defs: &BTreeMap<RefKey, Schema>,
) -> Vec<Schema> {
let mut result = Vec::new();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems possible for there to be literal or effective duplicates in this list.

Copy link
Author

@PolyProgrammist PolyProgrammist Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see there may be oneOf: [A, B] and oneOf: [C, D]
And maybe something like oneOf[{}, {type: string}] and oneOf[{type:string}, {type:string}] where for all of them the result would be {type:string}.

Do you think comparing like that is enough?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see there may be oneOf: [A, B] and oneOf: [C, D] And maybe something like oneOf[{}, {type: string}] and oneOf[{type:string}, {type:string}] where for all of them the result would be {type:string}.

That's not quite right: oneOf[{type: string}, {type: string}] would be equivalent to the schema false... and so therefore would the outer allOf.

but perhaps something like this?

{
  "allOf": [
    {
      "oneOf": [
        {
          "type": "object",
          "required": ["x"],
          "properties": {
            "x": { "const": 1 }
          }
        },
        {
          "type": "object",
          "required": ["x"],
          "properties": {
            "x": { "const": 2 }
          }
        }
      ]
    },
    {
      "oneOf": [
        {
          "type": "object",
          "required": ["y"],
          "properties": {
            "y": { "const": 3 }
          }
        },
        {
          "type": "object",
          "required": ["x", "y"],
          "properties": {
            "x": { "const": 1 },
            "y": { "const": 3 }
          }
        }
      ]
    }
  ]
}

I think the expansion you have in mind is more of less

{
  "oneOf": [
    {
      "allOf": [
        { "type": "object", "required": ["x"], "properties": { "x": { "const": 1 } } },
        { "type": "object", "required": ["y"], "properties": { "y": { "const": 3 } } }
      ]
    },
    {
      "allOf": [
        { "type": "object", "required": ["x"], "properties": { "x": { "const": 1 } } },
        { "type": "object", "required": ["x", "y"], "properties": { "x": { "const": 1 }, "y": { "const": 3 } } }
      ]
    },
    {
      "allOf": [
        { "type": "object", "required": ["x"], "properties": { "x": { "const": 2 } } },
        { "type": "object", "required": ["y"], "properties": { "y": { "const": 3 } } }
      ]
    },
    {
      "allOf": [
        { "type": "object", "required": ["x"], "properties": { "x": { "const": 2 } } },
        { "type": "object", "required": ["x", "y"], "properties": { "x": { "const": 1 }, "y": { "const": 3 } } }
      ]
    }
  ]
}

And if we further reduce that we get

{
  "oneOf": [
    {
      "type": "object",
      "required": ["x", "y"],
      "properties": {
        "x": { "const": 1 },
        "y": { "const": 3 }
      }
    },
    {
      "type": "object",
      "required": ["x", "y"],
      "properties": {
        "x": { "const": 1 },
        "y": { "const": 3 }
      }
    },
    {
      "type": "object",
      "required": ["x", "y"],
      "properties": {
        "x": { "const": 2 },
        "y": { "const": 3 }
      }
    },
    false
  ]
}

The first 2 are identical; the last one is unsatisfiable. I think we need check for exclusivity--perhaps by seeing if the A ∪ ⌐B is empty?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually that's not quite right and now I'm deeply uncertain about whether there might be duplicates.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I've had time to sleep on it:

If we have two sets of schemas, each known to consist of mutually exclusive schemas, then the cartesian product of those two sets must also be pairwise mutually exclusive. Why? A value that was valid for multiple schemas of the cartesian product would necessarily be valid for multiple schemas in (at least) one of the input schema sets (which would invalidate the initial premise).

However! I don't believe that typify currently ensures that oneOfs are mutually exclusive. Part of the (in-progress) typify rewrite is to ensure that that mutual exclusivity. For example, with an input like:

{
      "oneOf": [
        {
          "type": "object",
          "required": ["y"],
          "properties": {
            "y": { "const": 3 }
          }
        },
        {
          "type": "object",
          "required": ["x", "y"],
          "properties": {
            "x": { "const": 1 },
            "y": { "const": 3 }
          }
        }
      ]
    }

We would produce a type like this:

pub struct MyType {
    y: MyTypeY, // unit type that can only have a value of the integer 3
    x: MyTypeX, // basically serde_json::Value except *not* the integer 1
}

Which is all to say: this PR is wrong, but it's no more wrong that what we currently have and, indeed, it's quite a bit better in all but the most esoteric cases.

// Try to merge each pair of variants
if let Ok(merged) = try_merge_schema(base_variant, new_variant, defs) {
// Only include if the merge produced a satisfiable schema
if merged != Schema::Bool(false)
Copy link
Collaborator

@ahl ahl Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does try_merge_schema return false ever? Perhaps we should use merge_schema instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants