Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ConstrainedStr as dict keys #332

Merged
merged 5 commits into from
Dec 27, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ v0.17.0 (unreleased)
* prevent duplicate validator check in ipython, fix #312 by @samuelcolvin
* add "Using Pydantic" section to docs, #323 by @tiangolo & #326 by @samuelcolvin
* fix schema generation for fields annotated as ``: dict``, #330 by @nkonin
* add support for constrained strings as dict keys in schema, #332 by @tiangolo

v0.16.1 (2018-12-10)
....................
Expand Down
18 changes: 11 additions & 7 deletions pydantic/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ def get_long_model_name(model: Type['main.BaseModel']):
return f'{model.__module__}__{model.__name__}'.replace('.', '__')


def field_type_schema(
def field_type_schema( # noqa: C901 (ignore complexity)
field: Field,
*,
by_alias: bool,
Expand Down Expand Up @@ -405,16 +405,20 @@ def field_type_schema(
definitions.update(f_definitions)
return {'type': 'array', 'uniqueItems': True, 'items': f_schema}, definitions
elif field.shape is Shape.MAPPING:
dict_schema = {'type': 'object'}
regex = getattr(field.key_field.type_, 'regex', None)
f_schema, f_definitions = field_singleton_schema(
field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix
)
definitions.update(f_definitions)
if f_schema:
# The dict values are not simply Any
return {'type': 'object', 'additionalProperties': f_schema}, definitions
else:
# The dict values are Any, no need to declare it
return {'type': 'object'}, definitions
if regex:
# Dict keys have a regex pattern
# f_schema might be a schema or empty dict, add it either way
dict_schema['patternProperties'] = {regex.pattern: f_schema}
elif f_schema:
Copy link
Member

@samuelcolvin samuelcolvin Dec 27, 2018

Choose a reason for hiding this comment

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

Sorry, one more question.

Are these two cases really mutually exclusive? Eg. requiring elif rather than if.

maybe you might want both patternProperties and additionalProperties?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep. I thought about the same while implementing it.

The schema that would go in additionalProperties ends up as the value of the dict at patternProperties. That dict has as key the regex pattern, and as value the schema for the dict value.


Also from the spec:

Validation with "additionalProperties" applies only to the child values of instance names that do not match any names in "properties", and do not match any regular expression in "patternProperties".

Choose a reason for hiding this comment

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

I can think of a case that would allow for patternProperties and additionalProperties, but it is rather complex.

>>> from typing import Union, Dict, Any
>>> from pydantic import BaseModel, constr
>>>
>>> Identifier = constr(regex=r'^([a-zA-Z_][a-zA-Z0-9_]*)$')
>>> Slug = constr(regex=r'^([a-zA-Z_-][a-zA-Z0-9_-]*)$')
>>> Number = constr(regex=r'^\d+$')
>>>
>>> class IdentifierModel(BaseModel):
...     alias: str = ...
...
>>> class SlugModel(BaseModel):
...     alias: str = ...
...     value: str = ...
...
>>> class NumberModel(BaseModel):
...     alias: str = ...
...     value: int = ...
...
>>> class Model(BaseModel):
...     items: Union[Dict[Identifier, IdentifierModel], Dict[Slug, SlugModel], Dict[Number, NumberModel], Dict[str, Any]] = {}
...
>>> print(Model.schema_json(indent=2))
{
  "title": "Model",
  "type": "object",
  "properties": {
    "items": {
      "title": "Items",
      "default": {},
      "anyOf": [
        {
          "type": "object",
          "patternProperties": {
            "^([a-zA-Z_][a-zA-Z0-9_]*)$": {
              "$ref": "#/definitions/IdentifierModel"
            }
          }
        },
        {
          "type": "object",
          "patternProperties": {
            "^([a-zA-Z_-][a-zA-Z0-9_-]*)$": {
              "$ref": "#/definitions/SlugModel"
            }
          }
        },
        {
          "type": "object",
          "patternProperties": {
            "^\\d+$": {
              "$ref": "#/definitions/NumberModel"
            }
          }
        },
        {
          "type": "object"
        }
      ]
    }
  },
  "definitions": {
    "IdentifierModel": {
      "title": "IdentifierModel",
      "type": "object",
      "properties": {
        "alias": {
          "title": "Alias",
          "type": "string"
        }
      },
      "required": [
        "alias"
      ]
    },
    "SlugModel": {
      "title": "SlugModel",
      "type": "object",
      "properties": {
        "alias": {
          "title": "Alias",
          "type": "string"
        },
        "value": {
          "title": "Value",
          "type": "string"
        }
      },
      "required": [
        "alias",
        "value"
      ]
    },
    "NumberModel": {
      "title": "NumberModel",
      "type": "object",
      "properties": {
        "alias": {
          "title": "Alias",
          "type": "string"
        },
        "value": {
          "title": "Value",
          "type": "integer"
        }
      },
      "required": [
        "alias",
        "value"
      ]
    }
  }
}
# expected
{
  "title": "Model",
  "type": "object",
  "properties": {
    "items": {
      "title": "Items",
      "default": {},
      "patternProperties": {
        "^([a-zA-Z_][a-zA-Z0-9_]*)$": {
          "$ref": "#/definitions/IdentifierModel"
        },
        "^([a-zA-Z_-][a-zA-Z0-9_-]*)$": {
          "$ref": "#/definitions/SlugModel"
        },
        "^\\d+$": {
          "$ref": "#/definitions/NumberModel"
        }
      },
      "additionalProperties": {},
    }
  },
  "definitions": {
    "IdentifierModel": {
      "title": "IdentifierModel",
      "type": "object",
      "properties": {
        "alias": {
          "title": "Alias",
          "type": "string"
        }
      },
      "required": [
        "alias"
      ]
    },
    "SlugModel": {
      "title": "SlugModel",
      "type": "object",
      "properties": {
        "alias": {
          "title": "Alias",
          "type": "string"
        },
        "value": {
          "title": "Value",
          "type": "string"
        }
      },
      "required": [
        "alias",
        "value"
      ]
    },
    "NumberModel": {
      "title": "NumberModel",
      "type": "object",
      "properties": {
        "alias": {
          "title": "Alias",
          "type": "string"
        },
        "value": {
          "title": "Value",
          "type": "integer"
        }
      },
      "required": [
        "alias",
        "value"
      ]
    }
  }
}

Copy link
Member

Choose a reason for hiding this comment

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

Good find, however I think this is too niche for now, if you think it's a problem @demosdemon please create a new issue to discuss.

# The dict values are not simply Any, so they need a schema
dict_schema['additionalProperties'] = f_schema
return dict_schema, definitions
elif field.shape is Shape.TUPLE:
sub_schema = []
for sf in field.sub_fields:
Expand Down
17 changes: 17 additions & 0 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,3 +1019,20 @@ class Foo(BaseModel):
'type': 'object',
'properties': {'a': {'type': 'string', 'title': 'A', 'default': 'foo', 'examples': ['bar']}},
}


def test_schema_dict_constr():
regex_str = r'^([a-zA-Z_][a-zA-Z0-9_]*)$'
ConStrType = constr(regex=regex_str)
ConStrKeyDict = Dict[ConStrType, str]

class Foo(BaseModel):
a: ConStrKeyDict = {}

assert Foo.schema() == {
'title': 'Foo',
'type': 'object',
'properties': {
'a': {'type': 'object', 'title': 'A', 'default': {}, 'patternProperties': {regex_str: {'type': 'string'}}}
},
}