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

feat(schema): set minItems and maxItems in JSON schema for tuples #2497

Merged
merged 5 commits into from
Dec 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2497-PrettyWood.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Set `minItems` and `maxItems` in generated JSON schema for fixed-length tuples
9 changes: 8 additions & 1 deletion docs/build/schema_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,17 @@
'JSON Schema Validation',
'And equivalently for any other sub type, e.g. `List[int]`.'
],
[
'Tuple[str, ...]',
'array',
{'items': {'type': 'string'}},
'JSON Schema Validation',
'And equivalently for any other sub type, e.g. `Tuple[int, ...]`.'
],
[
'Tuple[str, int]',
'array',
{'items': [{'type': 'string'}, {'type': 'integer'}]},
{'items': [{'type': 'string'}, {'type': 'integer'}], 'minItems': 2, 'maxItems': 2},
'JSON Schema Validation',
(
'And equivalently for any other set of subtypes. Note: If using schemas for OpenAPI, '
Expand Down
30 changes: 21 additions & 9 deletions pydantic/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,15 +490,19 @@ def field_type_schema(
definitions.update(sf_definitions)
nested_models.update(sf_nested_models)
sub_schema.append(sf_schema)
if len(sub_schema) == 1:
if field.shape == SHAPE_GENERIC:
f_schema = sub_schema[0]
else:
f_schema = {'type': 'array', 'items': sub_schema[0]}
else:
f_schema = {'type': 'array', 'items': sub_schema}

sub_fields_len = len(sub_fields)
if field.shape == SHAPE_GENERIC:
f_schema = {'allOf': [f_schema]}
all_of_schemas = sub_schema[0] if sub_fields_len == 1 else {'type': 'array', 'items': sub_schema}
f_schema = {'allOf': [all_of_schemas]}
else:
f_schema = {
'type': 'array',
'minItems': sub_fields_len,
'maxItems': sub_fields_len,
}
if sub_fields_len >= 1:
f_schema['items'] = sub_schema
else:
assert field.shape in {SHAPE_SINGLETON, SHAPE_GENERIC}, field.shape
f_schema, f_definitions, f_nested_models = field_singleton_schema(
Expand Down Expand Up @@ -835,7 +839,15 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
ref_template=ref_template,
known_models=known_models,
)
f_schema.update({'type': 'array', 'items': list(sub_schema['properties'].values())})
items_schemas = list(sub_schema['properties'].values())
f_schema.update(
{
'type': 'array',
'items': items_schemas,
'minItems': len(items_schemas),
'maxItems': len(items_schemas),
}
)
elif not hasattr(field_type, '__pydantic_model__'):
add_field_type_to_schema(field_type, f_schema)

Expand Down
6 changes: 6 additions & 0 deletions tests/test_annotated_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class Model(BaseModel):
{'title': 'X', 'type': 'integer'},
{'title': 'Y', 'type': 'integer'},
],
'minItems': 2,
'maxItems': 2,
},
'pos2': {
'title': 'Pos2',
Expand All @@ -87,6 +89,8 @@ class Model(BaseModel):
{'title': 'X'},
{'title': 'Y'},
],
'minItems': 2,
'maxItems': 2,
},
'pos3': {
'title': 'Pos3',
Expand All @@ -95,6 +99,8 @@ class Model(BaseModel):
{'type': 'integer'},
{'type': 'integer'},
],
'minItems': 2,
'maxItems': 2,
},
},
'required': ['pos1', 'pos2', 'pos3'],
Expand Down
46 changes: 30 additions & 16 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,34 +526,36 @@ class Model(BaseModel):


@pytest.mark.parametrize(
'field_type,expected_schema',
'field_type,extra_props',
[
(tuple, {}),
(tuple, {'items': {}}),
(
Tuple[str, int, Union[str, int, float], float],
[
{'type': 'string'},
{'type': 'integer'},
{'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}]},
{'type': 'number'},
],
{
'items': [
{'type': 'string'},
{'type': 'integer'},
{'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}]},
{'type': 'number'},
],
'minItems': 4,
'maxItems': 4,
},
),
(Tuple[str], {'type': 'string'}),
(Tuple[str], {'items': [{'type': 'string'}], 'minItems': 1, 'maxItems': 1}),
(Tuple[()], {'maxItems': 0, 'minItems': 0}),
Copy link

Choose a reason for hiding this comment

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

Great! 👍

],
)
def test_tuple(field_type, expected_schema):
def test_tuple(field_type, extra_props):
class Model(BaseModel):
a: field_type

base_schema = {
assert Model.schema() == {
'title': 'Model',
'type': 'object',
'properties': {'a': {'title': 'A', 'type': 'array'}},
'properties': {'a': {'title': 'A', 'type': 'array', **extra_props}},
'required': ['a'],
}
base_schema['properties']['a']['items'] = expected_schema

assert Model.schema() == base_schema


def test_bool():
Expand Down Expand Up @@ -1923,6 +1925,8 @@ class Config:
{'exclusiveMinimum': 0, 'type': 'integer'},
{'exclusiveMinimum': 0, 'type': 'integer'},
],
'minItems': 3,
'maxItems': 3,
},
),
(
Expand Down Expand Up @@ -2312,6 +2316,8 @@ class LocationBase(BaseModel):
'default': Coordinates(x=0, y=0),
'type': 'array',
'items': [{'title': 'X', 'type': 'number'}, {'title': 'Y', 'type': 'number'}],
'minItems': 2,
'maxItems': 2,
}
},
}
Expand Down Expand Up @@ -2403,11 +2409,19 @@ class Model(BaseModel):
'examples': 'examples',
},
'data3': {'title': 'Data3', 'type': 'array', 'items': {}},
'data4': {'title': 'Data4', 'type': 'array', 'items': {'$ref': '#/definitions/CustomType'}},
'data4': {
'title': 'Data4',
'type': 'array',
'items': [{'$ref': '#/definitions/CustomType'}],
'minItems': 1,
'maxItems': 1,
},
'data5': {
'title': 'Data5',
'type': 'array',
'items': [{'$ref': '#/definitions/CustomType'}, {'type': 'string'}],
'minItems': 2,
'maxItems': 2,
},
},
'required': ['data0', 'data1', 'data2', 'data3', 'data4', 'data5'],
Expand Down