Pydantic: Required Optional fields

Created on 13 Nov 2019  Â·  14Comments  Â·  Source: samuelcolvin/pydantic

Question

Currently, adding an Optional field just removes the field from the generated required fields list in the schema/JSON schema. Is there a mechanism for having it instead generate an anyOf field requirement and requiring a field with a null value?

Please complete:

  • OS: Mac OS X
  • Python version import sys; print(sys.version): 3.7.4 (default, Oct 12 2019, 18:55:28)
  • Pydantic version import pydantic; print(pydantic.VERSION): 1.1

I couldn't find info on this in the help manual, though the general documentation for Union (link) doesn't specifically call out that it behaves differently with None.

from typing import Optional
import pydantic

class Metadata(pydantic.BaseModel):
    nullable_field = Optional[str]

Would love a way to generate {"anyOf": [{"type": "string"}, {"type": null}]}, though I realize that passing {} and {"nullable_field": None} will generate equivalent Pydantic models.

Thanks for the help and the library!

Change Feedback Wanted

Most helpful comment

I’m the kind of guy who is known to rant about optionals, nulls, zeros, empty strings, empty arrays and empty objects and how all of them are distinct cases. Would really like if all of the cases here and in various linked issues could be easily expressed in a Pydantic model and mapped to JSON Schema equivalents. I’ll try to summarize the current ways and workarounds to do it:

  • A field is required and cannot be null. — Works out of the box

    class Foo(BaseModel):
        foo: int
    
    Foo()
    # ValidationError: field required
    Foo(foo=42)
    # Foo(foo=42)
    Foo(foo=None)
    # ValidationError: none is not an allowed value
    Foo.schema()
    # {'properties': {'foo': {'title': 'Foo', 'type': 'integer'}},
    #  'required': ['foo'],
    #  'title': 'Foo',
    #  'type': 'object'}
    
  • A field is required and can be null. — Requires a schema patch

    class Foo(BaseModel):
        foo: Optional[int] = ...
    
        class Config:
            def schema_extra(schema, model):
                schema['properties']['foo'].update({'type': ['null', 'integer']})
    
    Foo()
    # ValidationError: field required
    Foo(foo=42)
    # Foo(foo=42)
    Foo(foo=None)
    # Foo(foo=None)
    Foo.schema()
    # {'properties': {'foo': {'title': 'Foo', 'type': ['null', 'integer']}},
    #  'required': ['foo'],
    #  'title': 'Foo',
    #  'type': 'object'}
    
  • A field is optional but if present cannot be null. — Requires a validator

    class Foo(BaseModel):
        foo: Optional[int]
    
        @validator('foo')
        def not_null(cls, v):
            if v is None:
                raise ValueError
            return v
    
    Foo()
    # Foo(foo=None)
    Foo(foo=42)
    # Foo(foo=42)
    Foo(foo=None)
    # ValidationError: type_error
    Foo.schema()
    # {'properties': {'foo': {'title': 'Foo', 'type': 'integer'}},
    #  'title': 'Foo',
    #  'type': 'object'}
    
  • A field is optional and may be null. — Requires a schema patch

    class Foo(BaseModel):
        foo: Optional[int]
    
        class Config:
            def schema_extra(schema, model):
                schema['properties']['foo'].update({'type': ['null', 'integer']})
    
    Foo()
    # Foo(foo=None)
    Foo(foo=42)
    # Foo(foo=42)
    Foo(foo=None)
    # Foo(foo=None)
    Foo.schema()
    # {'properties': {'foo': {'title': 'Foo', 'type': ['null', 'integer']}},
    #  'title': 'Foo',
    #  'type': 'object'}
    

Is that right, are those the easiest workarounds, and does the above cover all interesting cases?

(Cue OpenAPI users bickering that arrays in type will only be supported in 3.1+ while 3.0 uses a private extension "nullable": true.)

All 14 comments

Humm, the answer is "not really".

Currently the best solution might be allowing schema_extra to be a function as per #892. Then you could have a function that inspects required and modifies all non-required fields to add anyOf.

The reason for this is that although Optional[int] == Union[None, int], Optional generally has a different semantic meaning. I thought about changing it so that Fields marked nullable_field: Optional[int] e.g. without a default were required but could be None, however that would mean a field marked with the word "optional" was actually required, which is just too weird.

I guess in theory we could add a RequiredOptional type that meant you must supply a value to the field but it could be None. But I'm not sure how much work it would be.

@tiangolo or @dmontagu do you have opinion on this?

It seems a little unconventional/non-pythonic to me that Optional[X] gets a default value of None without that being explicitly specified, just because most related use cases would still treat it as required (eg dataclasses, regular function signatures, etc). I recognize the naming weirdness around the word Optional, but it just seems like the way python’s typing module works (and the way optionals work in most languages, as far as I’m aware).

That said, in practice having None as the default seems to be right in the vast majority of cases, so I don’t mind. It definitely removes a lot of = Nones.

Moreover, the schema currently generated by Optional is what I want in most cases, so if it is extra work to support both (and I suspect it would be), I’m glad it works the way it does now.

But I would be in favor of adding a RequiredOptional type that generated the schema described here. I’ve run into a few cases where I wanted this behavior in the past.

How about we do RequiredOptional now, and consider switching so that Optional fields are not implicitly optional in version 2?

Cue: someone comes along and abuses us with "Optional fields aren't optional! Rant Rant Rant". However, it is annoying not to have nullable fields which are required.

Other options:

  • use Nullable instead of RequiredOptional
  • have a config switch for whether Optional can be required

I think I’d prefer switching the behavior in v2 over adding a config setting. I’d also be okay leaving it as is; I’ll think about it. I just think config settings come with a lot of maintenance burden.

I’m not super worried about people saying Optional isn’t Optional, since, while yes that is maybe weird, for better or worse it’s also the convention everywhere else. It won’t surprise me if someone complains, but I think “convention” offers an easy response.

Nullable as a name sounds better to me than RequiredOptional 👍.

I think using Field(...) could be a way to specify that it's required, I guess it should do it, maybe we can implement it that way:

from typing import Optional
from pydantic import BaseModel, Field

class Metadata(BaseModel):
    nullable_field: Optional[str] = Field(...)

That would mean that the nullable_field can take str or None, but it has to have something (with the ...).

That's what I think I would imagine. (That independent of how we end up handling #1028).

Agreed, let's go with ... forcing a field to be required regardless of whether it's optional.

Then change the behaviour in v2 so that all fields without a default value are required regardless of whether they're optional.

About having Optional[x] with an implicit default of None, I'm OK with it. I'm OK with both options.

I can imagine how it could feel weird to have something without an explicit = None have a default None value.

But at the same time, I could imagine feeling weird about having something declared as Optional[x] that doesn't behave as "optional".

So, I guess it's kind of a fuzzy area... we'll get the rant anyway :joy:

Given that, I guess I would prefer to follow the convention in dataclasses, having Optional[x] still require a value. Just for convention, as @dmontagu says. But again, I'm OK with both.


About nullables / required-optional, we'll be able to achieve that with nullable: Optional[x] = ... or nullable: Optional[x] = Field(...) with #1031 .


That still leaves open the discussion for how Optional[x] vs Optional[x] = None should behave and if a Nullable[x]/RequiredOptional[x] is needed or not.

We'll still be able to achieve both functionalities with ... and None.

It seems that this change caused a regression in the latest release. The behavior between static and dynamic classes is different now.

from typing import Optional

from pydantic import create_model, BaseModel


def test_optional_value():
    class Test(BaseModel):
        field1: str
        field2: Optional[str]

    Test(field1="a", field2="b")
    Test(field1="a")


def test_dynamic_optional_value():
    fields = {'field1': (str, ...), 'field2': (Optional[str], ...)}
    Test = create_model("Test", __base__=BaseModel, **fields)

    Test(field1="a", field2="b")
    Test(field1="a")

With pydantic 1.1.1 both test cases succeed, with pydantic 1.2 the dynamic test case fails.

Is this expected? Based on the changelog and the documentation I am inclined to think it is not. Should I open a separate bug?

@bartv, thanks for reporting this. I've created a new issue #1047

I’m the kind of guy who is known to rant about optionals, nulls, zeros, empty strings, empty arrays and empty objects and how all of them are distinct cases. Would really like if all of the cases here and in various linked issues could be easily expressed in a Pydantic model and mapped to JSON Schema equivalents. I’ll try to summarize the current ways and workarounds to do it:

  • A field is required and cannot be null. — Works out of the box

    class Foo(BaseModel):
        foo: int
    
    Foo()
    # ValidationError: field required
    Foo(foo=42)
    # Foo(foo=42)
    Foo(foo=None)
    # ValidationError: none is not an allowed value
    Foo.schema()
    # {'properties': {'foo': {'title': 'Foo', 'type': 'integer'}},
    #  'required': ['foo'],
    #  'title': 'Foo',
    #  'type': 'object'}
    
  • A field is required and can be null. — Requires a schema patch

    class Foo(BaseModel):
        foo: Optional[int] = ...
    
        class Config:
            def schema_extra(schema, model):
                schema['properties']['foo'].update({'type': ['null', 'integer']})
    
    Foo()
    # ValidationError: field required
    Foo(foo=42)
    # Foo(foo=42)
    Foo(foo=None)
    # Foo(foo=None)
    Foo.schema()
    # {'properties': {'foo': {'title': 'Foo', 'type': ['null', 'integer']}},
    #  'required': ['foo'],
    #  'title': 'Foo',
    #  'type': 'object'}
    
  • A field is optional but if present cannot be null. — Requires a validator

    class Foo(BaseModel):
        foo: Optional[int]
    
        @validator('foo')
        def not_null(cls, v):
            if v is None:
                raise ValueError
            return v
    
    Foo()
    # Foo(foo=None)
    Foo(foo=42)
    # Foo(foo=42)
    Foo(foo=None)
    # ValidationError: type_error
    Foo.schema()
    # {'properties': {'foo': {'title': 'Foo', 'type': 'integer'}},
    #  'title': 'Foo',
    #  'type': 'object'}
    
  • A field is optional and may be null. — Requires a schema patch

    class Foo(BaseModel):
        foo: Optional[int]
    
        class Config:
            def schema_extra(schema, model):
                schema['properties']['foo'].update({'type': ['null', 'integer']})
    
    Foo()
    # Foo(foo=None)
    Foo(foo=42)
    # Foo(foo=42)
    Foo(foo=None)
    # Foo(foo=None)
    Foo.schema()
    # {'properties': {'foo': {'title': 'Foo', 'type': ['null', 'integer']}},
    #  'title': 'Foo',
    #  'type': 'object'}
    

Is that right, are those the easiest workarounds, and does the above cover all interesting cases?

(Cue OpenAPI users bickering that arrays in type will only be supported in 3.1+ while 3.0 uses a private extension "nullable": true.)

@yurikhan How would you implement "a field that may be omitted, but may not be null if the field is supplied"?

class Foo(BaseModel):
    foo: int = Field(default=None)

Foo()  # OK: Don't expect to see a ValidationError because only supplied fields are validated
Foo(foo=None)  # Not OK: Expect to see a ValidationError because `foo` is not an `int`, but no error raised

Hello @lsorber
One easy way is just to validate set values but not default one (so we don't use always=True in the validator)

class Foo(BaseModel):
    foo: int = Field(default=None)

    @validator('foo')
    def set_value_not_none(cls, v):
        assert v is not None
        return v

But the field is still set in the model. If we actually want the field not to be set at all it requires more work and probably use a custom Undefined class / object instead of None

Thanks for the quick response @PrettyWood! I was heading the same direction with a validator-based workaround, but just submitted an issue (https://github.com/samuelcolvin/pydantic/issues/1761) with a different validator-less workaround if you're interested.

@lsorber As far as I understand, in the current version a validator is the way to go. It is a minor annoyance that the field behaves as if set explicitly, but, because it cannot validly be None, testing for foo is None works.

In some places in my team’s code that doesn’t use Pydantic (yet?), we use a different sentinel value to represent missing keys.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kryft picture kryft  Â·  35Comments

chopraaa picture chopraaa  Â·  18Comments

dmfigol picture dmfigol  Â·  38Comments

marlonjan picture marlonjan  Â·  37Comments

bradodarb picture bradodarb  Â·  22Comments