1

I want to as the title says create a group of attributes a, b and c, such that any combination can be supplied as long as one is given. I have managed to achieve the functionality but it is not reflected in the schema which is what I can't manage to do.

from pydantic import BaseModel, root_validator

class Foo(BaseModel):
    a: str | None = None
    b: str | None = None
    c: str | None = None

    @root_validator
    def check_at_least_one_given(cls, values):
        if not any((values.get('a'), values.get('b'), values.get('c'))):
            raise ValueError("At least of a, b, or c must be given")
        return values

# Doesn't have required fields
print(Foo.schema_json(indent=2))
{
  "title": "Foo",
  "type": "object",
  "properties": {
    "a": {
      "title": "A",
      "type": "string"
    },
    "b": {
      "title": "B",
      "type": "string"
    },
    "c": {
      "title": "C",
      "type": "string"
    }
  }
}
# No error
print(Foo(a="1"))
>>> a='1' b=None c=None
print(Foo(b="2"))
>>> a=None b='2' c=None
print(Foo(c="3"))
>>> a=None b=None c='3'
print(Foo(a="1", b="2"))
>>> a='1' b='2' c=None
print(Foo(a="1", c="3"))
>>> a='1' b=None c='3'
print(Foo(b="2", c="3"))
>>> a=None b='2' c='3'
print(Foo(a="1", b="2", c="3"))
>>> a='1' b='2' c='3'

# Invalid
Foo()
>>> Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pydantic\main.py", line 342, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for Foo
__root__
  At least of a, b, or c must be given (type=value_error)

I want the schema to output something like

{
  "title": "Foo",
  "type": "object",
  "properties": {
    "a": {
      "title": "A",
      "type": "string"
    },
    "b": {
      "title": "B",
      "type": "string"
    },
    "c": {
      "title": "C",
      "type": "string"
    }
  },
  "required": [
    ["a", "b", "c"]
  ]
}

or something else that (probably) more clearly expresses the intent of at least one of these is required.

Is this possible and if so how is it done?

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41

1 Answers1

0

As far as I can tell, Pydantic does not have a built in mechanism for this. And the validation logic you provide in a custom validator will never find its way into the JSON schema. You could try and search through and if unsuccessful post a feature request for something like this in their issue tracker.

The JSON schema core specification defines the anyOf keyword, which takes subschemas to validate against. This allows specifying the required keyword once for each of your fields in its own subschema. See this answer for details and an example.

In the Pydantic Config you can utilize schema_extra to extend the auto-generated schema. Here is an example of how you can write a corresponding workaround:

from typing import Any

from pydantic import BaseModel, root_validator


class Foo(BaseModel):
    a: str | None = None
    b: str | None = None
    c: str | None = None

    @root_validator
    def check_at_least_one_given(cls, values: dict[str, Any]) -> dict[str, Any]:
        if all(
            (v is None for v in (values.get("a"), values.get("b"), values.get("c")))
        ):
            raise ValueError("Any one of `a`, `b`, or `c` must be given")
        return values

    class Config:
        @staticmethod
        def schema_extra(schema: dict[str, Any]) -> None:
            assert "anyOf" not in schema, "Oops! What now?"
            schema["anyOf"] = [
                {"required": ["a"]},
                {"required": ["b"]},
                {"required": ["c"]},
            ]
            for prop in schema.get("properties", {}).values():
                prop.pop("title", None)


if __name__ == "__main__":
    print(Foo.schema_json(indent=2))

Output:

{
  "title": "Foo",
  "type": "object",
  "properties": {
    "a": {
      "type": "string"
    },
    "b": {
      "type": "string"
    },
    "c": {
      "type": "string"
    }
  },
  "anyOf": [
    {
      "required": [
        "a"
      ]
    },
    {
      "required": [
        "b"
      ]
    },
    {
      "required": [
        "c"
      ]
    }
  ]
}

This conforms to the specs and expresses your custom validation.

But note that I put in that assert to indicate that I have no strong basis to assume that the automatic schema will not provide its own anyOf key at some point, which would greatly complicate things. Consider this an unstable solution.

Side note:

Be careful with the any check in your validator. An empty string is "falsy" just like None, which might lead to unexpected results, depending on whether you want to consider empty strings to be valid values in this context or not. any(v for v in ("", 0, False, None)) is False.

I adjusted your validator in my code to explicitly check against None for this reason.

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41