Repository: samuelcolvin/pydantic
Pull request #3247 (2021-09-23 09:56:43)

fix: collect only valid fields in mypy plugin
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- See https://pydantic-docs.helpmanual.io/contributing/ for help on Contributing -->

~EDIT: Sorry, I just noticed some tests are failing, so I'll switch it to draft PR now.~ (should be ready now)

## Change Summary

<!-- Please give a short summary of the changes. -->

In mypy plugin, currently it tries to find model fields by going through all the assignment statements in class body even if a variable name starts with underscore. When the plugin is used with the option `warn_untyped_fields` enabled, this leads to some of "normal" usage of class variables without type annotation (e.g. https://pydantic-docs.helpmanual.io/usage/validators/#reuse-validators) giving mypy error `error: Untyped fields disallowed  [pydantic-field]` (I think simple workaround would be to annotate with `ClassVar` though).

In this PR, I used `pydantic.utils.is_valid_field` to filter possible model fields before triggering errors for `warn_untyped_fields`, which essentially skips the variable name starting with underscore except `__root__` variable.

~I worried that skipping all the underscore-ed variable might have a bad effect on using private attributes, so in the unit test, I made sure its type is still inferred correctly.~ (EDIT: I couldn't find a way to test this cleanly. I tried `reveal_type` and `TYPE_CHECKING` but current test driver `test_mypy.py` didn't support them well).

Thanks for the code review!

## Related issue number

<!-- Are there any issues opened that will be resolved by merging this change? -->

Fixes https://github.com/samuelcolvin/pydantic/issues/3146

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [ ] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


hi-ogawa: Hello maintainers and contributors, sorry for stalling this PR a little bit.
I updated a test and I'd like it to be reviewed. Thanks a lot!

please review

hi-ogawa: I just noticed flake8 is failing due to additional if-branch I made:

```
 $ flake8 pydantic
pydantic/mypy.py:238:5: C901 'PydanticModelTransformer.collect_fields' is too complex (15)
```

Not sure what I should do for this since I think this PR should be really a few lines change and it shouldn't include additional refactoring. I would appreciate any guidance about this.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 39803a5fe..fd8151fda 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -131,7 +131,7 @@ jobs:
       fail-fast: false
       matrix:
         os: [macos, windows]
-        python-version: ['3.6', '3.7', '3.8', '3.9', '3.10.0-rc.2']
+        python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']
     env:
       PYTHON: ${{ matrix.python-version }}
       OS: ${{ matrix.os }}
diff --git a/HISTORY.md b/HISTORY.md
index 853177704..c362e6c77 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -60,7 +60,7 @@ for their kind support.
 * Improve field declaration for _pydantic_ `dataclass` by allowing the usage of _pydantic_ `Field` or `'metadata'` kwarg of `dataclasses.field`, #2384 by @PrettyWood
 * Making `typing-extensions` a required dependency, #2368 by @samuelcolvin
 * Make `resolve_annotations` more lenient, allowing for missing modules, #2363 by @samuelcolvin
-* Allow configuring models through class kwargs, #2356 by @MrMrRobat
+* Allow configuring models through class kwargs, #2356 by @Bobronium
 * Prevent `Mapping` subclasses from always being coerced to `dict`, #2325 by @ofek
 * fix: allow `None` for type `Optional[conset / conlist]`, #2320 by @PrettyWood
 * Support empty tuple type, #2318 by @PrettyWood
@@ -142,7 +142,7 @@ Thank you to pydantic's sponsors:
 
 ## v1.7.2 (2020-11-01)
 
-* fix slow `GenericModel` concrete model creation, allow `GenericModel` concrete name reusing in module, #2078 by @MrMrRobat
+* fix slow `GenericModel` concrete model creation, allow `GenericModel` concrete name reusing in module, #2078 by @Bobronium
 * keep the order of the fields when `validate_assignment` is set, #2073 by @PrettyWood
 * forward all the params of the stdlib `dataclass` when converted into _pydantic_ `dataclass`, #2065 by @PrettyWood
 
@@ -168,7 +168,7 @@ for their kind support.
 ### Highlights
 
 * python 3.9 support, thanks @PrettyWood
-* [Private model attributes](https://pydantic-docs.helpmanual.io/usage/models/#private-model-attributes), thanks @MrMrRobat
+* [Private model attributes](https://pydantic-docs.helpmanual.io/usage/models/#private-model-attributes), thanks @Bobronium
 * ["secrets files" support in `BaseSettings`](https://pydantic-docs.helpmanual.io/usage/settings/#secret-support), thanks @mdgilene
 * [convert stdlib dataclasses to pydantic dataclasses and use stdlib dataclasses in models](https://pydantic-docs.helpmanual.io/usage/dataclasses/#stdlib-dataclasses-and-pydantic-dataclasses), thanks @PrettyWood
 
@@ -205,8 +205,8 @@ for their kind support.
 * add `Enum` and `IntEnum` as valid types for fields, #1735 by @PrettyWood
 * Change default value of `__module__` argument of `create_model` from `None` to `'pydantic.main'`. 
   Set reference of created concrete model to it's module to allow pickling (not applied to models created in 
-  functions), #1686 by @MrMrRobat
-* Add private attributes support, #1679 by @MrMrRobat
+  functions), #1686 by @Bobronium
+* Add private attributes support, #1679 by @Bobronium
 * add `config` to `@validate_arguments`, #1663 by @samuelcolvin
 * Allow descendant Settings models to override env variable names for the fields defined in parent Settings models with 
   `env` in their `Config`. Previously only `env_prefix` configuration option was applicable, #1561 by @ojomio
@@ -257,7 +257,7 @@ Thank you to pydantic's sponsors: @matin, @tiangolo, @chdsbd, @jorgecarleitao, a
 * Avoid some side effects of `default_factory` by calling it only once
   if possible and by not setting a default value in the schema, #1491 by @PrettyWood
 * Added docs about dumping dataclasses to JSON, #1487 by @mikegrima
-* Make `BaseModel.__signature__` class-only, so getting `__signature__` from model instance will raise `AttributeError`, #1466 by @MrMrRobat
+* Make `BaseModel.__signature__` class-only, so getting `__signature__` from model instance will raise `AttributeError`, #1466 by @Bobronium
 * include `'format': 'password'` in the schema for secret types, #1424 by @atheuz
 * Modify schema constraints on `ConstrainedFloat` so that `exclusiveMinimum` and
   minimum are not included in the schema if they are equal to `-math.inf` and
@@ -306,8 +306,8 @@ Thank you to pydantic's sponsors: @matin, @tiangolo, @chdsbd, @jorgecarleitao, a
 * fix mypy signature for `root_validator`, #1192 by @samuelcolvin
 * Fixed parsing of nested 'custom root type' models, #1190 by @Shados
 * Add `validate_arguments` function decorator which checks the arguments to a function matches type annotations, #1179 by @samuelcolvin
-* Add `__signature__` to models, #1034 by @MrMrRobat
-* Refactor `._iter()` method, 10x speed boost for `dict(model)`, #1017 by @MrMrRobat
+* Add `__signature__` to models, #1034 by @Bobronium
+* Refactor `._iter()` method, 10x speed boost for `dict(model)`, #1017 by @Bobronium
 
 ## v1.4 (2020-01-24)
 
@@ -485,7 +485,7 @@ Thank you to pydantic's sponsors: @matin, @tiangolo, @chdsbd, @jorgecarleitao, a
 
 * add model name to `ValidationError` error message, #676 by @dmontagu
 * **breaking change**: remove `__getattr__` and rename `__values__` to `__dict__` on `BaseModel`,
-  deprecation warning on use `__values__` attr, attributes access speed increased up to 14 times, #712 by @MrMrRobat
+  deprecation warning on use `__values__` attr, attributes access speed increased up to 14 times, #712 by @Bobronium
 * support `ForwardRef` (without self-referencing annotations) in Python 3.6, #706 by @koxudaxi
 * implement `schema_extra` in `Config` sub-class, #663 by @tiangolo
 
@@ -498,12 +498,12 @@ Thank you to pydantic's sponsors: @matin, @tiangolo, @chdsbd, @jorgecarleitao, a
 
 * better support for floating point `multiple_of` values, #652 by @justindujardin
 * fix schema generation for `NewType` and `Literal`, #649 by @dmontagu
-* fix `alias_generator` and field config conflict, #645 by @gmetzker and #658 by @MrMrRobat
+* fix `alias_generator` and field config conflict, #645 by @gmetzker and #658 by @Bobronium
 * more detailed message for `EnumError`, #673 by @dmontagu
-* add advanced exclude support for `dict`, `json` and `copy`, #648 by @MrMrRobat
+* add advanced exclude support for `dict`, `json` and `copy`, #648 by @Bobronium
 * fix bug in `GenericModel` for models with concrete parameterized fields, #672 by @dmontagu
 * add documentation for `Literal` type, #651 by @dmontagu
-* add `Config.keep_untouched` for custom descriptors support, #679 by @MrMrRobat
+* add `Config.keep_untouched` for custom descriptors support, #679 by @Bobronium
 * use `inspect.cleandoc` internally to get model description, #657 by @tiangolo
 * add `Color` to schema generation, by @euri10
 * add documentation for Literal type, #651 by @dmontagu
@@ -520,7 +520,7 @@ Thank you to pydantic's sponsors: @matin, @tiangolo, @chdsbd, @jorgecarleitao, a
 * fix default values for `GenericModel`, #610 by @dmontagu
 * clarify that self-referencing models require python 3.7+, #616 by @vlcinsky
 * fix truncate for types, #611 by @dmontagu
-* add `alias_generator` support, #622 by @MrMrRobat
+* add `alias_generator` support, #622 by @Bobronium
 * fix unparameterized generic type schema generation, #625 by @dmontagu
 * fix schema generation with multiple/circular references to the same model, #621 by @tiangolo and @wongpat
 * support custom root types, #628 by @koxudaxi
diff --git a/benchmarks/requirements.txt b/benchmarks/requirements.txt
index d32a10313..cc576cfa7 100644
--- a/benchmarks/requirements.txt
+++ b/benchmarks/requirements.txt
@@ -1,5 +1,5 @@
 # no versions since benchmarks should always run with the most recent version of each package
-devtools==0.7.0
+devtools==0.8.0
 
 python-dateutil
 trafaret
diff --git a/changes/1897-PrettyWood.md b/changes/1897-PrettyWood.md
new file mode 100644
index 000000000..b539418d6
--- /dev/null
+++ b/changes/1897-PrettyWood.md
@@ -0,0 +1,1 @@
+add `confrozenset()`, analogous to `conset()` and `conlist()`
diff --git a/changes/2007-diabolo-dan.md b/changes/2007-diabolo-dan.md
new file mode 100644
index 000000000..d79c056b0
--- /dev/null
+++ b/changes/2007-diabolo-dan.md
@@ -0,0 +1,1 @@
+Add parameterised subclasses to `__bases__` when constructing new parameterised classes, so that `A <: B => A[int] <: B[int]`.
diff --git a/changes/2092-PrettyWood.md b/changes/2092-PrettyWood.md
new file mode 100644
index 000000000..a975710d1
--- /dev/null
+++ b/changes/2092-PrettyWood.md
@@ -0,0 +1,1 @@
+add `Config.smart_union` to prevent coercion in `Union` if possible. See [the doc](https://pydantic-docs.helpmanual.io/usage/model_config/#smart-union) for more information
diff --git a/changes/2497-PrettyWood.md b/changes/2497-PrettyWood.md
new file mode 100644
index 000000000..0e28a966a
--- /dev/null
+++ b/changes/2497-PrettyWood.md
@@ -0,0 +1,1 @@
+Set `minItems` and `maxItems` in generated JSON schema for fixed-length tuples
diff --git a/changes/2588-uriyyo.md b/changes/2588-uriyyo.md
new file mode 100644
index 000000000..938d28267
--- /dev/null
+++ b/changes/2588-uriyyo.md
@@ -0,0 +1,1 @@
+Try to evaluate forward refs automatically at model creation.
\ No newline at end of file
diff --git a/changes/2650-PrettyWood.md b/changes/2650-PrettyWood.md
new file mode 100644
index 000000000..61e31128a
--- /dev/null
+++ b/changes/2650-PrettyWood.md
@@ -0,0 +1,1 @@
+Make serialization of referenced pydantic models possible
diff --git a/changes/2697-sammchardy.md b/changes/2697-sammchardy.md
new file mode 100644
index 000000000..72ce6e03c
--- /dev/null
+++ b/changes/2697-sammchardy.md
@@ -0,0 +1,1 @@
+`Enum` fields now properly support extra kwargs in schema generation
\ No newline at end of file
diff --git a/changes/2917-davidmreed.md b/changes/2917-davidmreed.md
new file mode 100644
index 000000000..243781b56
--- /dev/null
+++ b/changes/2917-davidmreed.md
@@ -0,0 +1,1 @@
+Make `SecretsSettingsSource` parse values being assigned to fields of complex types when sourced from a secrets file, just as when sourced from environment variables.
diff --git a/changes/3122-PrettyWood.md b/changes/3122-PrettyWood.md
new file mode 100644
index 000000000..b9dc1c245
--- /dev/null
+++ b/changes/3122-PrettyWood.md
@@ -0,0 +1,1 @@
+avoid dict coercion when using dict subclasses as field type
\ No newline at end of file
diff --git a/changes/3146-hi-ogawa.md b/changes/3146-hi-ogawa.md
new file mode 100644
index 000000000..d0f3cb475
--- /dev/null
+++ b/changes/3146-hi-ogawa.md
@@ -0,0 +1,1 @@
+Fix mypy plugin to collect fields based on `pydantic.utils.is_valid_field` so that it ignores untyped private variables
diff --git a/changes/3190-joaommartins.md b/changes/3190-joaommartins.md
new file mode 100644
index 000000000..74af4f13a
--- /dev/null
+++ b/changes/3190-joaommartins.md
@@ -0,0 +1,1 @@
+Always use `Enum` value as default in generated JSON schema.
\ No newline at end of file
diff --git a/changes/3401-uriyyo.md b/changes/3401-uriyyo.md
new file mode 100644
index 000000000..7b46117f5
--- /dev/null
+++ b/changes/3401-uriyyo.md
@@ -0,0 +1,1 @@
+Fix issue when pydantic fail to parse `typing.ClassVar` string type annotation.
\ No newline at end of file
diff --git a/docs/benchmarks.md b/docs/benchmarks.md
index ba1b89712..94364bf34 100644
--- a/docs/benchmarks.md
+++ b/docs/benchmarks.md
@@ -5,4 +5,4 @@ Below are the results of crude benchmarks comparing *pydantic* to other validati
 See [the benchmarks code](https://github.com/samuelcolvin/pydantic/tree/master/benchmarks)
 for more details on the test case. Feel free to suggest more packages to benchmark or improve an existing one.
 
-Benchmarks were run with Python 3.8.6 and the package versions listed above installed via pypi on macOS Bug Sur.
+Benchmarks were run with Python 3.8.6 and the package versions listed above installed via pypi on macOS Big Sur.
diff --git a/docs/build/schema_mapping.py b/docs/build/schema_mapping.py
index 4f204b358..c9b6c018f 100755
--- a/docs/build/schema_mapping.py
+++ b/docs/build/schema_mapping.py
@@ -81,10 +81,17 @@ table = [
         '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, '
diff --git a/docs/examples/exporting_models_json_forward_ref.py b/docs/examples/exporting_models_json_forward_ref.py
new file mode 100644
index 000000000..540221f3a
--- /dev/null
+++ b/docs/examples/exporting_models_json_forward_ref.py
@@ -0,0 +1,34 @@
+from typing import List, Optional
+
+from pydantic import BaseModel
+
+
+class Address(BaseModel):
+    city: str
+    country: str
+
+
+class User(BaseModel):
+    name: str
+    address: Address
+    friends: Optional[List['User']] = None
+
+    class Config:
+        json_encoders = {
+            Address: lambda a: f'{a.city} ({a.country})',
+            'User': lambda u: f'{u.name} in {u.address.city} '
+                              f'({u.address.country[:2].upper()})',
+        }
+
+
+User.update_forward_refs()
+
+wolfgang = User(
+    name='Wolfgang',
+    address=Address(city='Berlin', country='Deutschland'),
+    friends=[
+        User(name='Pierre', address=Address(city='Paris', country='France')),
+        User(name='John', address=Address(city='London', country='UK')),
+    ],
+)
+print(wolfgang.json())
diff --git a/docs/examples/model_config_smart_union_off.py b/docs/examples/model_config_smart_union_off.py
new file mode 100644
index 000000000..f31ead0f2
--- /dev/null
+++ b/docs/examples/model_config_smart_union_off.py
@@ -0,0 +1,19 @@
+from typing import Union
+
+from pydantic import BaseModel
+
+
+class Foo(BaseModel):
+    pass
+
+
+class Bar(BaseModel):
+    pass
+
+
+class Model(BaseModel):
+    x: Union[str, int]
+    y: Union[Foo, Bar]
+
+
+print(Model(x=1, y=Bar()))
diff --git a/docs/examples/model_config_smart_union_on.py b/docs/examples/model_config_smart_union_on.py
new file mode 100644
index 000000000..91f7ac32c
--- /dev/null
+++ b/docs/examples/model_config_smart_union_on.py
@@ -0,0 +1,22 @@
+from typing import Union
+
+from pydantic import BaseModel
+
+
+class Foo(BaseModel):
+    pass
+
+
+class Bar(BaseModel):
+    pass
+
+
+class Model(BaseModel):
+    x: Union[str, int]
+    y: Union[Foo, Bar]
+
+    class Config:
+        smart_union = True
+
+
+print(Model(x=1, y=Bar()))
diff --git a/docs/examples/model_config_smart_union_on_edge_case.py b/docs/examples/model_config_smart_union_on_edge_case.py
new file mode 100644
index 000000000..1a2c6c2e3
--- /dev/null
+++ b/docs/examples/model_config_smart_union_on_edge_case.py
@@ -0,0 +1,14 @@
+from typing import List, Union
+
+from pydantic import BaseModel
+
+
+class Model(BaseModel, smart_union=True):
+    x: Union[List[str], List[int]]
+
+
+# Expected coercion
+print(Model(x=[1, '2']))
+
+# Unexpected coercion
+print(Model(x=[1, 2]))
diff --git a/docs/examples/models_generics_naming.py b/docs/examples/models_generics_naming.py
index e55742b40..641c90623 100644
--- a/docs/examples/models_generics_naming.py
+++ b/docs/examples/models_generics_naming.py
@@ -13,5 +13,5 @@ class Response(GenericModel, Generic[DataT]):
         return f'{params[0].__name__.title()}Response'
 
 
-print(Response[int](data=1))
-print(Response[str](data='a'))
+print(repr(Response[int](data=1)))
+print(repr(Response[str](data='a')))
diff --git a/docs/examples/postponed_annotations_self_referencing_annotations.py b/docs/examples/postponed_annotations_self_referencing_annotations.py
index 902bd62e3..aa8966c1c 100644
--- a/docs/examples/postponed_annotations_self_referencing_annotations.py
+++ b/docs/examples/postponed_annotations_self_referencing_annotations.py
@@ -8,7 +8,5 @@ class Foo(BaseModel):
     sibling: Foo = None
 
 
-Foo.update_forward_refs()
-
 print(Foo())
 print(Foo(sibling={'a': '321'}))
diff --git a/docs/examples/postponed_annotations_self_referencing_string.py b/docs/examples/postponed_annotations_self_referencing_string.py
index 2c918a2ca..3290051bb 100644
--- a/docs/examples/postponed_annotations_self_referencing_string.py
+++ b/docs/examples/postponed_annotations_self_referencing_string.py
@@ -7,7 +7,5 @@ class Foo(BaseModel):
     sibling: 'Foo' = None
 
 
-Foo.update_forward_refs()
-
 print(Foo())
 print(Foo(sibling={'a': '321'}))
diff --git a/docs/mypy_plugin.md b/docs/mypy_plugin.md
index 842078056..437454b4e 100644
--- a/docs/mypy_plugin.md
+++ b/docs/mypy_plugin.md
@@ -55,8 +55,8 @@ There are other benefits too! See below for more details.
   cf. [ORM mode](usage/models.md#orm-mode-aka-arbitrary-class-instances)
 
 #### Generate a signature for `dataclasses`
-* classes decorated with [`@pydantic.dataclasess.dataclass`](usage/dataclasses.md) are type checked the same as standard python dataclasses
-* The `@pydantic.dataclasess.dataclass` decorator accepts a `config` keyword argument which has the same meaning as [the `Config` sub-class](usage/model_config.md).
+* classes decorated with [`@pydantic.dataclasses.dataclass`](usage/dataclasses.md) are type checked the same as standard python dataclasses
+* The `@pydantic.dataclasses.dataclass` decorator accepts a `config` keyword argument which has the same meaning as [the `Config` sub-class](usage/model_config.md).
 
 ### Optional Capabilities:
 #### Prevent the use of required dynamic aliases
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 2f1520969..dee12cff6 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,12 +1,12 @@
 ansi2html==1.6.0
-flake8==3.9.2
-flake8-quotes==3.3.0
-hypothesis==6.18.0
+flake8==4.0.1
+flake8-quotes==3.3.1
+hypothesis==6.30.1
 markdown-include==0.6.0
 mdx-truly-sane-lists==1.2
-mkdocs==1.1.2
+mkdocs==1.2.3
 mkdocs-exclude==1.0.2
-mkdocs-material==7.1.6
+mkdocs-material==8.0.5
 sqlalchemy
 orjson
 ujson
diff --git a/docs/usage/exporting_models.md b/docs/usage/exporting_models.md
index 7c21d520c..f5770daa4 100644
--- a/docs/usage/exporting_models.md
+++ b/docs/usage/exporting_models.md
@@ -88,7 +88,7 @@ _(This script is complete, it should run "as is")_
 
 ### `json_encoders`
 
-Serialisation can be customised on a model using the `json_encoders` config property; the keys should be types, and
+Serialisation can be customised on a model using the `json_encoders` config property; the keys should be types (or names of types for forward references), and
 the values should be functions which serialise that type (see the example below):
 
 ```py
@@ -107,6 +107,14 @@ encoders taking precedence over the parent one.
 ```
 _(This script is complete, it should run "as is")_
 
+### Serialising self-reference or other models
+
+In case of forward references, you can use a string with the class name instead of the class itself
+```py
+{!.tmp_examples/exporting_models_json_forward_ref.py!}
+```
+_(This script is complete, it should run "as is")_
+
 ### Serialising subclasses
 
 !!! note
diff --git a/docs/usage/model_config.md b/docs/usage/model_config.md
index ccfb7c9ff..a3f55fec9 100644
--- a/docs/usage/model_config.md
+++ b/docs/usage/model_config.md
@@ -113,7 +113,10 @@ not be included in the model schemas. **Note**: this means that attributes on th
 : whether to treat any underscore non-class var attrs as private, or leave them as is; See [Private model attributes](models.md#private-model-attributes)
 
 **`copy_on_model_validation`**
-: whether or not inherited models used as fields should be reconstructed (copied) on validation instead of being kept untouched (default: `True`)
+: whether inherited models used as fields should be reconstructed (copied) on validation instead of being kept untouched (default: `True`)
+
+**`smart_union`**
+: whether _pydantic_ should try to check all types inside `Union` to prevent undesired coercion (see [the dedicated section](#smart-union)
 
 ## Change behaviour globally
 
@@ -164,3 +167,30 @@ For example:
 {!.tmp_examples/model_config_alias_precedence.py!}
 ```
 _(This script is complete, it should run "as is")_
+
+## Smart Union
+
+By default, as explained [here](types.md#unions), _pydantic_ tries to validate (and coerce if it can) in the order of the `Union`.
+So sometimes you may have unexpected coerced data.
+
+```py
+{!.tmp_examples/model_config_smart_union_off.py!}
+```
+_(This script is complete, it should run "as is")_
+
+To prevent this, you can enable `Config.smart_union`. _Pydantic_ will then check all allowed types before even trying to coerce.
+Know that this is of course slower, especially if your `Union` is quite big.
+
+```py
+{!.tmp_examples/model_config_smart_union_on.py!}
+```
+_(This script is complete, it should run "as is")_
+
+!!! warning
+    Note that this option **does not support compound types yet** (e.g. differentiate `List[int]` and `List[str]`).
+    This option will be improved further once a strict mode is added in _pydantic_ and will probably be the default behaviour in v2!
+
+```py
+{!.tmp_examples/model_config_smart_union_on_edge_case.py!}
+```
+_(This script is complete, it should run "as is")_
diff --git a/docs/usage/models.md b/docs/usage/models.md
index 94ae244be..072038d41 100644
--- a/docs/usage/models.md
+++ b/docs/usage/models.md
@@ -381,8 +381,8 @@ the `create_model` method to allow models to be created on the fly.
 Here `StaticFoobarModel` and `DynamicFoobarModel` are identical.
 
 !!! warning
-    See the note in [Required Optional Fields](#required-optional-fields) for the distinct between an ellipsis as a
-    field default and annotation only fields. 
+    See the note in [Required Optional Fields](#required-optional-fields) for the distinction between an ellipsis as a
+    field default and annotation-only fields. 
     See [samuelcolvin/pydantic#1047](https://github.com/samuelcolvin/pydantic/issues/1047) for more details.
 
 Fields are defined by either a tuple of the form `(<type>, <default value>)` or just a default value. The
diff --git a/docs/usage/postponed_annotations.md b/docs/usage/postponed_annotations.md
index cc445880b..8e975e262 100644
--- a/docs/usage/postponed_annotations.md
+++ b/docs/usage/postponed_annotations.md
@@ -45,9 +45,8 @@ Resolving this is beyond the call for *pydantic*: either remove the future impor
 
 ## Self-referencing Models
 
-Data structures with self-referencing models are also supported, provided the function
-`update_forward_refs()` is called once the model is created (you will be reminded
-with a friendly error message if you forget).
+Data structures with self-referencing models are also supported. Self-referencing fields will be automatically
+resolved after model creation.
 
 Within the model, you can refer to the not-yet-constructed model using a string:
 
diff --git a/docs/usage/schema.md b/docs/usage/schema.md
index d49bb631b..9c3419611 100644
--- a/docs/usage/schema.md
+++ b/docs/usage/schema.md
@@ -37,7 +37,7 @@ The format of `$ref`s (`"#/definitions/FooBar"` above) can be altered by calling
 with the `ref_template` keyword argument, e.g. `ApplePie.schema(ref_template='/schemas/{model}.json#/')`, here `{model}`
 will be replaced with the model naming using `str.format()`.
 
-## Field customisation
+## Field customization
 
 Optionally, the `Field` function can be used to provide extra information about the field and validations.
 It has the following arguments:
diff --git a/docs/usage/types.md b/docs/usage/types.md
index 439e2f695..37d779468 100644
--- a/docs/usage/types.md
+++ b/docs/usage/types.md
@@ -70,7 +70,7 @@ with custom properties and validation.
 : see [Datetime Types](#datetime-types) below for more detail on parsing and validation
 
 `typing.Any`
-: allows any value include `None`, thus an `Any` field is optional
+: allows any value including `None`, thus an `Any` field is optional
 
 `typing.Annotated`
 : allows wrapping another type with arbitrary metadata, as per [PEP-593](https://www.python.org/dev/peps/pep-0593/). The
@@ -234,21 +234,33 @@ _(This script is complete, it should run "as is")_
 
 The `Union` type allows a model attribute to accept different types, e.g.:
 
-!!! warning
-    This script is complete, it should run "as is". However, it may not reflect the desired behavior; see below.
+!!! info
+    You may get unexpected coercion with `Union`; see below.<br />
+    Know that you can also make the check slower but stricter by using [Smart Union](model_config.md#smart-union)
 
 ```py
 {!.tmp_examples/types_union_incorrect.py!}
 ```
+_(This script is complete, it should run "as is")_
 
 However, as can be seen above, *pydantic* will attempt to 'match' any of the types defined under `Union` and will use
 the first one that matches. In the above example the `id` of `user_03` was defined as a `uuid.UUID` class (which
 is defined under the attribute's `Union` annotation) but as the `uuid.UUID` can be marshalled into an `int` it
 chose to match against the `int` type and disregarded the other types.
 
+!!! warning
+    `typing.Union` also ignores order when [defined](https://docs.python.org/3/library/typing.html#typing.Union),
+    so `Union[int, float] == Union[float, int]` which can lead to unexpected behaviour
+    when combined with matching based on the `Union` type order inside other type definitions, such as `List` and `Dict`
+    types (because python treats these definitions as singletons).
+    For example, `Dict[str, Union[int, float]] == Dict[str, Union[float, int]]` with the order based on the first time it was defined.
+    Please note that this can also be [affected by third party libraries](https://github.com/samuelcolvin/pydantic/issues/2835)
+    and their internal type definitions and the import orders.
+
 As such, it is recommended that, when defining `Union` annotations, the most specific type is included first and
-followed by less specific types. In the above example, the `UUID` class should precede the `int` and `str`
-classes to preclude the unexpected representation as such:
+followed by less specific types.
+
+In the above example, the `UUID` class should precede the `int` and `str` classes to preclude the unexpected representation as such:
 
 ```py
 {!.tmp_examples/types_union_correct.py!}
@@ -565,6 +577,10 @@ _(This script is complete, it should run "as is")_
 : type method for constraining sets;
   see [Constrained Types](#constrained-types)
 
+`confrozenset`
+: type method for constraining frozen sets;
+  see [Constrained Types](#constrained-types)
+
 `constr`
 : type method for constraining strs;
   see [Constrained Types](#constrained-types)
@@ -792,6 +808,13 @@ The following arguments are available when using the `conset` type function
 - `min_items: int = None`: minimum number of items in the set
 - `max_items: int = None`: maximum number of items in the set
 
+### Arguments to `confrozenset`
+The following arguments are available when using the `confrozenset` type function
+
+- `item_type: Type[T]`: type of the frozenset items
+- `min_items: int = None`: minimum number of items in the frozenset
+- `max_items: int = None`: maximum number of items in the frozenset
+
 ### Arguments to `conint`
 The following arguments are available when using the `conint` type function
 
diff --git a/mkdocs.yml b/mkdocs.yml
index fc97d1d6d..537bca4e3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -27,9 +27,10 @@ theme:
 
 repo_name: samuelcolvin/pydantic
 repo_url: https://github.com/samuelcolvin/pydantic
-google_analytics:
-- 'UA-62733018-4'
-- 'auto'
+extra:
+  analytics:
+    provider: google
+    property: UA-62733018-4
 
 extra_css:
 - 'extra/terminal.css'
diff --git a/pydantic/__init__.py b/pydantic/__init__.py
index 191514821..ac7f558a5 100644
--- a/pydantic/__init__.py
+++ b/pydantic/__init__.py
@@ -78,6 +78,8 @@ __all__ = [
     'conlist',
     'ConstrainedSet',
     'conset',
+    'ConstrainedFrozenSet',
+    'confrozenset',
     'ConstrainedStr',
     'constr',
     'PyObject',
diff --git a/pydantic/config.py b/pydantic/config.py
index 5cc7e3a74..646e60ca8 100644
--- a/pydantic/config.py
+++ b/pydantic/config.py
@@ -24,7 +24,6 @@ if TYPE_CHECKING:
         def __call__(self, schema: Dict[str, Any], model_class: Type[BaseModel]) -> None:
             pass
 
-
 else:
     SchemaExtraCallable = Callable[..., None]
 
@@ -63,8 +62,10 @@ class BaseConfig:
     json_encoders: Dict[Type[Any], AnyCallable] = {}
     underscore_attrs_are_private: bool = False
 
-    # Whether or not inherited models as fields should be reconstructed as base model
+    # whether inherited models as fields should be reconstructed as base model
     copy_on_model_validation: bool = True
+    # whether `Union` should check all allowed types before even trying to coerce
+    smart_union: bool = False
 
     @classmethod
     def get_field_info(cls, name: str) -> Dict[str, Any]:
diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py
index c5ba09b36..b68aba8d1 100644
--- a/pydantic/env_settings.py
+++ b/pydantic/env_settings.py
@@ -6,7 +6,7 @@ from typing import AbstractSet, Any, Callable, ClassVar, Dict, List, Mapping, Op
 from .config import BaseConfig, Extra
 from .fields import ModelField
 from .main import BaseModel
-from .typing import StrPath, display_as_type, get_origin, is_union_origin
+from .typing import StrPath, display_as_type, get_origin, is_union
 from .utils import deep_update, path_type, sequence_like
 
 env_file_sentinel = str(object())
@@ -175,9 +175,7 @@ class EnvSettingsSource:
                 except ValueError as e:
                     raise SettingsError(f'error parsing JSON for "{env_name}"') from e
             elif (
-                is_union_origin(get_origin(field.type_))
-                and field.sub_fields
-                and any(f.is_complex() for f in field.sub_fields)
+                is_union(get_origin(field.type_)) and field.sub_fields and any(f.is_complex() for f in field.sub_fields)
             ):
                 try:
                     env_val = settings.__config__.json_loads(env_val)
@@ -218,7 +216,14 @@ class SecretsSettingsSource:
             for env_name in field.field_info.extra['env_names']:
                 path = secrets_path / env_name
                 if path.is_file():
-                    secrets[field.alias] = path.read_text().strip()
+                    secret_value = path.read_text().strip()
+                    if field.is_complex():
+                        try:
+                            secret_value = settings.__config__.json_loads(secret_value)
+                        except ValueError as e:
+                            raise SettingsError(f'error parsing JSON for "{env_name}"') from e
+
+                    secrets[field.alias] = secret_value
                 elif path.exists():
                     warnings.warn(
                         f'attempted to load secret file "{path}" but found a {path_type(path)} instead.',
diff --git a/pydantic/errors.py b/pydantic/errors.py
index 1c129b923..1b17cf12a 100644
--- a/pydantic/errors.py
+++ b/pydantic/errors.py
@@ -36,7 +36,6 @@ __all__ = (
     'IntegerError',
     'FloatError',
     'PathError',
-    '_PathValueError',
     'PathNotExistsError',
     'PathNotAFileError',
     'PathNotADirectoryError',
@@ -49,11 +48,14 @@ __all__ = (
     'TupleLengthError',
     'ListMinLengthError',
     'ListMaxLengthError',
+    'SetMinLengthError',
+    'SetMaxLengthError',
+    'FrozenSetMinLengthError',
+    'FrozenSetMaxLengthError',
     'AnyStrMinLengthError',
     'AnyStrMaxLengthError',
     'StrError',
     'StrRegexError',
-    '_NumberBoundError',
     'NumberNotGtError',
     'NumberNotGeError',
     'NumberNotLtError',
@@ -338,6 +340,22 @@ class SetMaxLengthError(PydanticValueError):
         super().__init__(limit_value=limit_value)
 
 
+class FrozenSetMinLengthError(PydanticValueError):
+    code = 'frozenset.min_items'
+    msg_template = 'ensure this value has at least {limit_value} items'
+
+    def __init__(self, *, limit_value: int) -> None:
+        super().__init__(limit_value=limit_value)
+
+
+class FrozenSetMaxLengthError(PydanticValueError):
+    code = 'frozenset.max_items'
+    msg_template = 'ensure this value has at most {limit_value} items'
+
+    def __init__(self, *, limit_value: int) -> None:
+        super().__init__(limit_value=limit_value)
+
+
 class AnyStrMinLengthError(PydanticValueError):
     code = 'any_str.min_length'
     msg_template = 'ensure this value has at least {limit_value} characters'
diff --git a/pydantic/fields.py b/pydantic/fields.py
index a15d0ab58..8894158aa 100644
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -42,7 +42,7 @@ from .typing import (
     is_new_type,
     is_none_type,
     is_typeddict,
-    is_union_origin,
+    is_union,
     new_type_supertype,
 )
 from .utils import PyObjectStr, Representation, ValueItems, lenient_issubclass, sequence_like, smart_deepcopy
@@ -554,13 +554,13 @@ class ModelField(Representation):
             if isinstance(self.type_, type) and isinstance(None, self.type_):
                 self.allow_none = True
             return
-        if origin is Annotated:
+        elif origin is Annotated:
             self.type_ = get_args(self.type_)[0]
             self._type_analysis()
             return
-        if origin is Callable:
+        elif origin is Callable:
             return
-        if is_union_origin(origin):
+        elif is_union(origin):
             types_ = []
             for type_ in get_args(self.type_):
                 if type_ is NoneType:
@@ -580,8 +580,7 @@ class ModelField(Representation):
             else:
                 self.sub_fields = [self._create_sub_type(t, f'{self.name}_{display_as_type(t)}') for t in types_]
             return
-
-        if issubclass(origin, Tuple):  # type: ignore
+        elif issubclass(origin, Tuple):  # type: ignore
             # origin == Tuple without item type
             args = get_args(self.type_)
             if not args:  # plain tuple
@@ -599,8 +598,7 @@ class ModelField(Representation):
                 self.shape = SHAPE_TUPLE
                 self.sub_fields = [self._create_sub_type(t, f'{self.name}_{i}') for i, t in enumerate(args)]
             return
-
-        if issubclass(origin, List):
+        elif issubclass(origin, List):
             # Create self validators
             get_validators = getattr(self.type_, '__get_validators__', None)
             if get_validators:
@@ -621,6 +619,13 @@ class ModelField(Representation):
             self.type_ = get_args(self.type_)[0]
             self.shape = SHAPE_SET
         elif issubclass(origin, FrozenSet):
+            # Create self validators
+            get_validators = getattr(self.type_, '__get_validators__', None)
+            if get_validators:
+                self.class_validators.update(
+                    {f'frozenset_{i}': Validator(validator, pre=True) for i, validator in enumerate(get_validators())}
+                )
+
             self.type_ = get_args(self.type_)[0]
             self.shape = SHAPE_FROZENSET
         elif issubclass(origin, Deque):
@@ -629,6 +634,11 @@ class ModelField(Representation):
         elif issubclass(origin, Sequence):
             self.type_ = get_args(self.type_)[0]
             self.shape = SHAPE_SEQUENCE
+        # priority to most common mapping: dict
+        elif origin is dict or origin is Dict:
+            self.key_field = self._create_sub_type(get_args(self.type_)[0], 'key_' + self.name, for_keys=True)
+            self.type_ = get_args(self.type_)[1]
+            self.shape = SHAPE_DICT
         elif issubclass(origin, DefaultDict):
             self.key_field = self._create_sub_type(get_args(self.type_)[0], 'key_' + self.name, for_keys=True)
             self.type_ = get_args(self.type_)[1]
@@ -637,10 +647,6 @@ class ModelField(Representation):
             self.key_field = self._create_sub_type(get_args(self.type_)[0], 'key_' + self.name, for_keys=True)
             self.type_ = int
             self.shape = SHAPE_COUNTER
-        elif issubclass(origin, Dict):
-            self.key_field = self._create_sub_type(get_args(self.type_)[0], 'key_' + self.name, for_keys=True)
-            self.type_ = get_args(self.type_)[1]
-            self.shape = SHAPE_DICT
         elif issubclass(origin, Mapping):
             self.key_field = self._create_sub_type(get_args(self.type_)[0], 'key_' + self.name, for_keys=True)
             self.type_ = get_args(self.type_)[1]
@@ -935,6 +941,35 @@ class ModelField(Representation):
     ) -> 'ValidateReturn':
         if self.sub_fields:
             errors = []
+
+            if self.model_config.smart_union and is_union(get_origin(self.type_)):
+                # 1st pass: check if the value is an exact instance of one of the Union types
+                # (e.g. to avoid coercing a bool into an int)
+                for field in self.sub_fields:
+                    if v.__class__ is field.outer_type_:
+                        return v, None
+
+                # 2nd pass: check if the value is an instance of any subclass of the Union types
+                for field in self.sub_fields:
+                    # This whole logic will be improved later on to support more complex `isinstance` checks
+                    # It will probably be done once a strict mode is added and be something like:
+                    # ```
+                    #     value, error = field.validate(v, values, strict=True)
+                    #     if error is None:
+                    #         return value, None
+                    # ```
+                    try:
+                        if isinstance(v, field.outer_type_):
+                            return v, None
+                    except TypeError:
+                        # compound type
+                        if isinstance(v, get_origin(field.outer_type_)):
+                            value, error = field.validate(v, values, loc=loc, cls=cls)
+                            if not error:
+                                return value, None
+
+            # 1st pass by default or 3rd pass with `smart_union` enabled:
+            # check if the value can be coerced into one of the Union types
             for field in self.sub_fields:
                 value, error = field.validate(v, values, loc=loc, cls=cls)
                 if error:
diff --git a/pydantic/generics.py b/pydantic/generics.py
index 17d782feb..1fee636c3 100644
--- a/pydantic/generics.py
+++ b/pydantic/generics.py
@@ -30,6 +30,15 @@ _generic_types_cache: Dict[Tuple[Type[Any], Union[Any, Tuple[Any, ...]]], Type[B
 GenericModelT = TypeVar('GenericModelT', bound='GenericModel')
 TypeVarType = Any  # since mypy doesn't allow the use of TypeVar as a type
 
+Parametrization = Mapping[TypeVarType, Type[Any]]
+
+# _assigned_parameters is a Mapping from parametrized version of generic models to assigned types of parametrizations
+# as captured during construction of the class (not instances).
+# E.g., for generic model `Model[A, B]`, when parametrized model `Model[int, str]` is created,
+# `Model[int, str]`: {A: int, B: str}` will be stored in `_assigned_parameters`.
+# (This information is only otherwise available after creation from the class name string).
+_assigned_parameters: Dict[Type[Any], Parametrization] = {}
+
 
 class GenericModel(BaseModel):
     __slots__ = ()
@@ -86,13 +95,15 @@ class GenericModel(BaseModel):
             create_model(
                 model_name,
                 __module__=model_module or cls.__module__,
-                __base__=cls,
+                __base__=(cls,) + tuple(cls.__parameterized_bases__(typevars_map)),
                 __config__=None,
                 __validators__=validators,
                 **fields,
             ),
         )
 
+        _assigned_parameters[created_model] = typevars_map
+
         if called_globally:  # create global reference and therefore allow pickling
             object_by_reference = None
             reference_name = model_name
@@ -142,6 +153,70 @@ class GenericModel(BaseModel):
         params_component = ', '.join(param_names)
         return f'{cls.__name__}[{params_component}]'
 
+    @classmethod
+    def __parameterized_bases__(cls, typevars_map: Parametrization) -> Iterator[Type[Any]]:
+        """
+        Returns unbound bases of cls parameterised to given type variables
+
+        :param typevars_map: Dictionary of type applications for binding subclasses.
+            Given a generic class `Model` with 2 type variables [S, T]
+            and a concrete model `Model[str, int]`,
+            the value `{S: str, T: int}` would be passed to `typevars_map`.
+        :return: an iterator of generic sub classes, parameterised by `typevars_map`
+            and other assigned parameters of `cls`
+
+        e.g.:
+        ```
+        class A(GenericModel, Generic[T]):
+            ...
+
+        class B(A[V], Generic[V]):
+            ...
+
+        assert A[int] in B.__parameterized_bases__({V: int})
+        ```
+        """
+
+        def build_base_model(
+            base_model: Type[GenericModel], mapped_types: Parametrization
+        ) -> Iterator[Type[GenericModel]]:
+            base_parameters = tuple([mapped_types[param] for param in base_model.__parameters__])
+            parameterized_base = base_model.__class_getitem__(base_parameters)
+            if parameterized_base is base_model or parameterized_base is cls:
+                # Avoid duplication in MRO
+                return
+            yield parameterized_base
+
+        for base_model in cls.__bases__:
+            if not issubclass(base_model, GenericModel):
+                # not a class that can be meaningfully parameterized
+                continue
+            elif not getattr(base_model, '__parameters__', None):
+                # base_model is "GenericModel"  (and has no __parameters__)
+                # or
+                # base_model is already concrete, and will be included transitively via cls.
+                continue
+            elif cls in _assigned_parameters:
+                if base_model in _assigned_parameters:
+                    # cls is partially parameterised but not from base_model
+                    # e.g. cls = B[S], base_model = A[S]
+                    # B[S][int] should subclass A[int],  (and will be transitively via B[int])
+                    # but it's not viable to consistently subclass types with arbitrary construction
+                    # So don't attempt to include A[S][int]
+                    continue
+                else:  # base_model not in _assigned_parameters:
+                    # cls is partially parameterized, base_model is original generic
+                    # e.g.  cls = B[str, T], base_model = B[S, T]
+                    # Need to determine the mapping for the base_model parameters
+                    mapped_types: Parametrization = {
+                        key: typevars_map.get(value, value) for key, value in _assigned_parameters[cls].items()
+                    }
+                    yield from build_base_model(base_model, mapped_types)
+            else:
+                # cls is base generic, so base_class has a distinct base
+                # can construct the Parameterised base model using typevars_map directly
+                yield from build_base_model(base_model, typevars_map)
+
 
 def replace_types(type_: Any, type_map: Mapping[Any, Any]) -> Any:
     """Return type with all occurrences of `type_map` keys recursively replaced with their values.
diff --git a/pydantic/json.py b/pydantic/json.py
index 7cd33cafe..d03f3b042 100644
--- a/pydantic/json.py
+++ b/pydantic/json.py
@@ -103,7 +103,11 @@ def custom_pydantic_encoder(type_encoders: Dict[Any, Callable[[Type[Any]], Any]]
         try:
             encoder = type_encoders[base]
         except KeyError:
-            continue
+            try:
+                encoder = type_encoders[base.__name__]
+            except KeyError:
+                continue
+
         return encoder(obj)
     else:  # We have exited the for loop without finding a suitable encoder
         return pydantic_encoder(obj)
diff --git a/pydantic/main.py b/pydantic/main.py
index a25e96efe..02bebc460 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -1,4 +1,3 @@
-import sys
 import warnings
 from abc import ABCMeta
 from copy import deepcopy
@@ -40,9 +39,9 @@ from .typing import (
     get_origin,
     is_classvar,
     is_namedtuple,
-    is_union_origin,
+    is_union,
     resolve_annotations,
-    update_field_forward_refs,
+    update_model_forward_refs,
 )
 from .utils import (
     ROOT_KEY,
@@ -192,7 +191,7 @@ class ModelMetaclass(ABCMeta):
                 elif is_valid_field(ann_name):
                     validate_field_name(bases, ann_name)
                     value = namespace.get(ann_name, Undefined)
-                    allowed_types = get_args(ann_type) if is_union_origin(get_origin(ann_type)) else (ann_type,)
+                    allowed_types = get_args(ann_type) if is_union(get_origin(ann_type)) else (ann_type,)
                     if (
                         is_untouched(value)
                         and ann_type != PyObject
@@ -289,6 +288,8 @@ class ModelMetaclass(ABCMeta):
         cls = super().__new__(mcs, name, bases, new_namespace, **kwargs)
         # set __signature__ attr only for model class, but not for its instances
         cls.__signature__ = ClassAttribute('__signature__', generate_model_signature(cls.__init__, fields, config))
+        cls.__try_update_forward_refs__()
+
         return cls
 
 
@@ -468,13 +469,19 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
             )
             exclude_unset = skip_defaults
         encoder = cast(Callable[[Any], Any], encoder or self.__json_encoder__)
-        data = self.dict(
-            include=include,
-            exclude=exclude,
-            by_alias=by_alias,
-            exclude_unset=exclude_unset,
-            exclude_defaults=exclude_defaults,
-            exclude_none=exclude_none,
+
+        # We don't directly call `self.dict()`, which does exactly the same thing but
+        # with `to_dict = True` because we want to keep raw `BaseModel` instances and not as `dict`.
+        # This allows users to write custom JSON encoders for given `BaseModel` classes.
+        data = dict(
+            self._iter(
+                by_alias=by_alias,
+                include=include,
+                exclude=exclude,
+                exclude_unset=exclude_unset,
+                exclude_defaults=exclude_defaults,
+                exclude_none=exclude_none,
+            )
         )
         if self.__custom_root_type__:
             data = data[ROOT_KEY]
@@ -746,15 +753,20 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
         else:
             return v
 
+    @classmethod
+    def __try_update_forward_refs__(cls) -> None:
+        """
+        Same as update_forward_refs but will not raise exception
+        when forward references are not defined.
+        """
+        update_model_forward_refs(cls, cls.__fields__.values(), {}, (NameError,))
+
     @classmethod
     def update_forward_refs(cls, **localns: Any) -> None:
         """
         Try to update ForwardRefs on fields based on this Model, globalns and localns.
         """
-        globalns = sys.modules[cls.__module__].__dict__.copy()
-        globalns.setdefault(cls.__name__, cls)
-        for f in cls.__fields__.values():
-            update_field_forward_refs(f, globalns=globalns, localns=localns)
+        update_model_forward_refs(cls, cls.__fields__.values(), localns)
 
     def __iter__(self) -> 'TupleGenerator':
         """
@@ -877,7 +889,7 @@ def create_model(
     __model_name: str,
     *,
     __config__: Optional[Type[BaseConfig]] = None,
-    __base__: Type['Model'],
+    __base__: Union[Type['Model'], Tuple[Type['Model'], ...]],
     __module__: str = __name__,
     __validators__: Dict[str, classmethod] = None,
     **field_definitions: Any,
@@ -889,7 +901,7 @@ def create_model(
     __model_name: str,
     *,
     __config__: Optional[Type[BaseConfig]] = None,
-    __base__: Optional[Type['Model']] = None,
+    __base__: Union[None, Type['Model'], Tuple[Type['Model'], ...]] = None,
     __module__: str = __name__,
     __validators__: Dict[str, classmethod] = None,
     **field_definitions: Any,
@@ -910,8 +922,10 @@ def create_model(
     if __base__ is not None:
         if __config__ is not None:
             raise ConfigError('to avoid confusion __config__ and __base__ cannot be used together')
+        if not isinstance(__base__, tuple):
+            __base__ = (__base__,)
     else:
-        __base__ = cast(Type['Model'], BaseModel)
+        __base__ = (cast(Type['Model'], BaseModel),)
 
     fields = {}
     annotations = {}
@@ -942,7 +956,7 @@ def create_model(
     if __config__:
         namespace['Config'] = inherit_config(__config__, BaseConfig)
 
-    return type(__model_name, (__base__,), namespace)
+    return type(__model_name, __base__, namespace)
 
 
 _missing = object()
diff --git a/pydantic/mypy.py b/pydantic/mypy.py
index 1dfd49e60..9aa0878a8 100644
--- a/pydantic/mypy.py
+++ b/pydantic/mypy.py
@@ -1,6 +1,8 @@
 from configparser import ConfigParser
 from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type as TypingType, Union
 
+from pydantic.utils import is_valid_field
+
 try:
     import toml
 except ImportError:  # pragma: no cover
@@ -247,7 +249,7 @@ class PydanticModelTransformer:
                 continue
 
             lhs = stmt.lvalues[0]
-            if not isinstance(lhs, NameExpr):
+            if not isinstance(lhs, NameExpr) or not is_valid_field(lhs.name):
                 continue
 
             if not stmt.new_syntax and self.plugin_config.warn_untyped_fields:
diff --git a/pydantic/networks.py b/pydantic/networks.py
index 557edc835..2be1ac80d 100644
--- a/pydantic/networks.py
+++ b/pydantic/networks.py
@@ -12,8 +12,8 @@ from ipaddress import (
 from typing import (
     TYPE_CHECKING,
     Any,
+    Collection,
     Dict,
-    FrozenSet,
     Generator,
     Optional,
     Pattern,
@@ -51,7 +51,6 @@ if TYPE_CHECKING:
         query: Optional[str]
         fragment: Optional[str]
 
-
 else:
     email_validator = None
 
@@ -123,7 +122,7 @@ class AnyUrl(str):
     strip_whitespace = True
     min_length = 1
     max_length = 2 ** 16
-    allowed_schemes: Optional[Set[str]] = None
+    allowed_schemes: Optional[Collection[str]] = None
     tld_required: bool = False
     user_required: bool = False
     host_required: bool = True
@@ -249,7 +248,7 @@ class AnyUrl(str):
             raise errors.UrlSchemeError()
 
         if cls.allowed_schemes and scheme.lower() not in cls.allowed_schemes:
-            raise errors.UrlSchemePermittedError(cls.allowed_schemes)
+            raise errors.UrlSchemePermittedError(set(cls.allowed_schemes))
 
         port = parts['port']
         if port is not None and int(port) > 65_535:
@@ -383,7 +382,7 @@ def stricturl(
     max_length: int = 2 ** 16,
     tld_required: bool = True,
     host_required: bool = True,
-    allowed_schemes: Optional[Union[FrozenSet[str], Set[str]]] = None,
+    allowed_schemes: Optional[Collection[str]] = None,
 ) -> Type[AnyUrl]:
     # use kwargs then define conf in a dict to aid with IDE type hinting
     namespace = dict(
diff --git a/pydantic/schema.py b/pydantic/schema.py
index 581b248d3..727fc7274 100644
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -49,6 +49,7 @@ from .networks import AnyUrl, EmailStr
 from .types import (
     ConstrainedDecimal,
     ConstrainedFloat,
+    ConstrainedFrozenSet,
     ConstrainedInt,
     ConstrainedList,
     ConstrainedSet,
@@ -58,6 +59,7 @@ from .types import (
     conbytes,
     condecimal,
     confloat,
+    confrozenset,
     conint,
     conlist,
     conset,
@@ -72,7 +74,7 @@ from .typing import (
     is_literal_type,
     is_namedtuple,
     is_none_type,
-    is_union_origin,
+    is_union,
 )
 from .utils import ROOT_KEY, get_model, lenient_issubclass, sequence_like
 
@@ -178,8 +180,7 @@ def model_schema(
     return m_schema
 
 
-def get_field_info_schema(field: ModelField) -> Tuple[Dict[str, Any], bool]:
-    schema_overrides = False
+def get_field_info_schema(field: ModelField, schema_overrides: bool = False) -> Tuple[Dict[str, Any], bool]:
 
     # If no title is explicitly set, we don't set title in the schema for enums.
     # The behaviour is the same as `BaseModel` reference, where the default title
@@ -499,15 +500,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(
@@ -832,7 +837,7 @@ def field_singleton_schema(  # noqa: C901 (ignore complexity)
         add_field_type_to_schema(field_type, f_schema)
     elif lenient_issubclass(field_type, Enum):
         enum_name = model_name_map[field_type]
-        f_schema, schema_overrides = get_field_info_schema(field)
+        f_schema, schema_overrides = get_field_info_schema(field, schema_overrides)
         f_schema.update(get_schema_ref(enum_name, ref_prefix, ref_template, schema_overrides))
         definitions[enum_name] = enum_process_schema(field_type)
     elif is_namedtuple(field_type):
@@ -844,7 +849,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)
 
@@ -909,7 +922,9 @@ def multitypes_literal_field_for_schema(values: Tuple[Any, ...], field: ModelFie
 
 
 def encode_default(dft: Any) -> Any:
-    if isinstance(dft, (int, float, str)):
+    if isinstance(dft, Enum):
+        return dft.value
+    elif isinstance(dft, (int, float, str)):
         return dft
     elif sequence_like(dft):
         t = dft.__class__
@@ -971,7 +986,7 @@ def get_annotation_with_constraints(annotation: Any, field_info: FieldInfo) -> T
         if (
             is_literal_type(type_)
             or isinstance(type_, ForwardRef)
-            or lenient_issubclass(type_, (ConstrainedList, ConstrainedSet))
+            or lenient_issubclass(type_, (ConstrainedList, ConstrainedSet, ConstrainedFrozenSet))
         ):
             return type_
         origin = get_origin(type_)
@@ -983,7 +998,7 @@ def get_annotation_with_constraints(annotation: Any, field_info: FieldInfo) -> T
 
             if origin is Annotated:
                 return go(args[0])
-            if is_union_origin(origin):
+            if is_union(origin):
                 return Union[tuple(go(a) for a in args)]  # type: ignore
 
             if issubclass(origin, List) and (field_info.min_items is not None or field_info.max_items is not None):
@@ -994,6 +1009,10 @@ def get_annotation_with_constraints(annotation: Any, field_info: FieldInfo) -> T
                 used_constraints.update({'min_items', 'max_items'})
                 return conset(go(args[0]), min_items=field_info.min_items, max_items=field_info.max_items)
 
+            if issubclass(origin, FrozenSet) and (field_info.min_items is not None or field_info.max_items is not None):
+                used_constraints.update({'min_items', 'max_items'})
+                return confrozenset(go(args[0]), min_items=field_info.min_items, max_items=field_info.max_items)
+
             for t in (Tuple, List, Set, FrozenSet, Sequence):
                 if issubclass(origin, t):  # type: ignore
                     return t[tuple(go(a) for a in args)]  # type: ignore
@@ -1017,7 +1036,16 @@ def get_annotation_with_constraints(annotation: Any, field_info: FieldInfo) -> T
                 attrs = ('max_length', 'min_length', 'regex')
                 constraint_func = conbytes
             elif issubclass(type_, numeric_types) and not issubclass(
-                type_, (ConstrainedInt, ConstrainedFloat, ConstrainedDecimal, ConstrainedList, ConstrainedSet, bool)
+                type_,
+                (
+                    ConstrainedInt,
+                    ConstrainedFloat,
+                    ConstrainedDecimal,
+                    ConstrainedList,
+                    ConstrainedSet,
+                    ConstrainedFrozenSet,
+                    bool,
+                ),
             ):
                 # Is numeric type
                 attrs = ('gt', 'lt', 'ge', 'le', 'multiple_of')
diff --git a/pydantic/types.py b/pydantic/types.py
index ca7ada5f5..a6dff2ad3 100644
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -12,6 +12,7 @@ from typing import (
     Callable,
     ClassVar,
     Dict,
+    FrozenSet,
     List,
     Optional,
     Pattern,
@@ -36,6 +37,7 @@ from .validators import (
     constr_strip_whitespace,
     decimal_validator,
     float_validator,
+    frozenset_validator,
     int_validator,
     list_validator,
     number_multiple_validator,
@@ -62,6 +64,8 @@ __all__ = [
     'conlist',
     'ConstrainedSet',
     'conset',
+    'ConstrainedFrozenSet',
+    'confrozenset',
     'ConstrainedStr',
     'constr',
     'PyObject',
@@ -484,6 +488,48 @@ def conset(item_type: Type[T], *, min_items: int = None, max_items: int = None)
     return new_class('ConstrainedSetValue', (ConstrainedSet,), {}, lambda ns: ns.update(namespace))
 
 
+# This types superclass should be FrozenSet[T], but cython chokes on that...
+class ConstrainedFrozenSet(frozenset):  # type: ignore
+    # Needed for pydantic to detect that this is a set
+    __origin__ = frozenset
+    __args__: FrozenSet[Type[T]]  # type: ignore
+
+    min_items: Optional[int] = None
+    max_items: Optional[int] = None
+    item_type: Type[T]  # type: ignore
+
+    @classmethod
+    def __get_validators__(cls) -> 'CallableGenerator':
+        yield cls.frozenset_length_validator
+
+    @classmethod
+    def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
+        update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items)
+
+    @classmethod
+    def frozenset_length_validator(cls, v: 'Optional[FrozenSet[T]]') -> 'Optional[FrozenSet[T]]':
+        if v is None:
+            return None
+
+        v = frozenset_validator(v)
+        v_len = len(v)
+
+        if cls.min_items is not None and v_len < cls.min_items:
+            raise errors.FrozenSetMinLengthError(limit_value=cls.min_items)
+
+        if cls.max_items is not None and v_len > cls.max_items:
+            raise errors.FrozenSetMaxLengthError(limit_value=cls.max_items)
+
+        return v
+
+
+def confrozenset(item_type: Type[T], *, min_items: int = None, max_items: int = None) -> Type[FrozenSet[T]]:
+    # __args__ is needed to conform to typing generics api
+    namespace = {'min_items': min_items, 'max_items': max_items, 'item_type': item_type, '__args__': [item_type]}
+    # We use new_class to be able to deal with Generic types
+    return new_class('ConstrainedFrozenSetValue', (ConstrainedFrozenSet,), {}, lambda ns: ns.update(namespace))
+
+
 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LIST TYPES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 # This types superclass should be List[T], but cython chokes on that...
diff --git a/pydantic/typing.py b/pydantic/typing.py
index 156db0b5f..409b35a0c 100644
--- a/pydantic/typing.py
+++ b/pydantic/typing.py
@@ -7,6 +7,7 @@ from typing import (  # type: ignore
     ClassVar,
     Dict,
     Generator,
+    Iterable,
     List,
     Mapping,
     NewType,
@@ -56,13 +57,11 @@ if sys.version_info < (3, 7):
     def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
         return type_._eval_type(globalns, localns)
 
-
 elif sys.version_info < (3, 9):
 
     def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
         return type_._evaluate(globalns, localns)
 
-
 else:
 
     def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
@@ -108,7 +107,6 @@ if sys.version_info < (3, 8):
             return cast(Type[Any], Annotated)  # mypy complains about _SpecialForm in py3.6
         return getattr(t, '__origin__', None)
 
-
 else:
     from typing import get_origin as _typing_get_origin
 
@@ -139,7 +137,6 @@ if sys.version_info < (3, 7):  # noqa: C901 (ignore complexity)
             return t.__args__ + t.__metadata__
         return getattr(t, '__args__', ())
 
-
 elif sys.version_info < (3, 8):  # noqa: C901
     from typing import _GenericAlias
 
@@ -158,7 +155,6 @@ elif sys.version_info < (3, 8):  # noqa: C901
             return res
         return getattr(t, '__args__', ())
 
-
 else:
     from typing import get_args as _typing_get_args
 
@@ -192,7 +188,7 @@ else:
 
 if sys.version_info < (3, 10):
 
-    def is_union_origin(tp: Type[Any]) -> bool:
+    def is_union(tp: Type[Any]) -> bool:
         return tp is Union
 
     WithArgsTypes = (TypingGenericAlias,)
@@ -201,8 +197,8 @@ else:
     import types
     import typing
 
-    def is_union_origin(origin: Type[Any]) -> bool:
-        return origin is Union or origin is types.UnionType  # noqa: E721
+    def is_union(tp: Type[Any]) -> bool:
+        return tp is Union or tp is types.UnionType  # noqa: E721
 
     WithArgsTypes = (typing._GenericAlias, types.GenericAlias, types.UnionType)
 
@@ -251,6 +247,7 @@ __all__ = (
     'new_type_supertype',
     'is_classvar',
     'update_field_forward_refs',
+    'update_model_forward_refs',
     'TupleGenerator',
     'DictStrAny',
     'DictAny',
@@ -267,7 +264,7 @@ __all__ = (
     'get_origin',
     'typing_base',
     'get_all_type_hints',
-    'is_union_origin',
+    'is_union',
     'StrPath',
 )
 
@@ -278,7 +275,7 @@ NoneType = None.__class__
 NONE_TYPES: Tuple[Any, Any, Any] = (None, NoneType, Literal[None])
 
 
-if sys.version_info < (3, 8):  # noqa: C901 (ignore complexity)
+if sys.version_info < (3, 8):
     # Even though this implementation is slower, we need it for python 3.6/3.7:
     # In python 3.6/3.7 "Literal" is not a builtin type and uses a different
     # mechanism.
@@ -288,6 +285,17 @@ if sys.version_info < (3, 8):  # noqa: C901 (ignore complexity)
     def is_none_type(type_: Any) -> bool:
         return type_ in NONE_TYPES
 
+elif sys.version_info[:2] == (3, 8):
+    # We can use the fast implementation for 3.8 but there is a very weird bug
+    # where it can fail for `Literal[None]`.
+    # We just need to redefine a useless `Literal[None]` inside the function body to fix this
+
+    def is_none_type(type_: Any) -> bool:
+        Literal[None]  # fix edge case
+        for none_type in NONE_TYPES:
+            if type_ is none_type:
+                return True
+        return False
 
 else:
 
@@ -332,7 +340,9 @@ def resolve_annotations(raw_annotations: Dict[str, Type[Any]], module_name: Opti
     annotations = {}
     for name, value in raw_annotations.items():
         if isinstance(value, str):
-            if sys.version_info >= (3, 7):
+            if (3, 10) > sys.version_info >= (3, 9, 8) or sys.version_info >= (3, 10, 1):
+                value = ForwardRef(value, is_argument=False, is_class=True)
+            elif sys.version_info >= (3, 7):
                 value = ForwardRef(value, is_argument=False)
             else:
                 value = ForwardRef(value)
@@ -357,7 +367,6 @@ if sys.version_info >= (3, 7):
     def literal_values(type_: Type[Any]) -> Tuple[Any, ...]:
         return get_args(type_)
 
-
 else:
 
     def is_literal_type(type_: Type[Any]) -> bool:
@@ -439,6 +448,29 @@ def update_field_forward_refs(field: 'ModelField', globalns: Any, localns: Any)
             update_field_forward_refs(sub_f, globalns=globalns, localns=localns)
 
 
+def update_model_forward_refs(
+    model: Type[Any],
+    fields: Iterable['ModelField'],
+    localns: 'DictStrAny',
+    exc_to_suppress: Tuple[Type[BaseException], ...] = (),
+) -> None:
+    """
+    Try to update model fields ForwardRefs based on model and localns.
+    """
+    if model.__module__ in sys.modules:
+        globalns = sys.modules[model.__module__].__dict__.copy()
+    else:
+        globalns = {}
+
+    globalns.setdefault(model.__name__, model)
+
+    for f in fields:
+        try:
+            update_field_forward_refs(f, globalns=globalns, localns=localns)
+        except exc_to_suppress:
+            pass
+
+
 def get_class(type_: Type[Any]) -> Union[None, bool, Type[Any]]:
     """
     Tries to get the class of a Type[T] annotation. Returns True if Type is used
diff --git a/requirements.txt b/requirements.txt
index 694a57870..5a2b15ce9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,8 @@
 # requirements for compilation and from setup.py so dependabot prompts us to test with latest version of these packages
 
-Cython==0.29.24;sys_platform!='win32'
-devtools==0.7.0
+Cython==0.29.25;sys_platform!='win32'
+devtools==0.8.0
 email-validator==1.1.3
 dataclasses==0.6; python_version < '3.7'
-typing-extensions==3.10.0.2
-python-dotenv==0.19.0
+typing-extensions==4.0.1
+python-dotenv==0.19.2


Test patch:
diff --git a/tests/mypy/modules/plugin_success.py b/tests/mypy/modules/plugin_success.py
index 6fb6d6336..217eebcd3 100644
--- a/tests/mypy/modules/plugin_success.py
+++ b/tests/mypy/modules/plugin_success.py
@@ -1,6 +1,6 @@
 from typing import ClassVar, Optional, Union
 
-from pydantic import BaseModel, BaseSettings, Field, create_model
+from pydantic import BaseModel, BaseSettings, Field, create_model, validator
 from pydantic.dataclasses import dataclass
 
 
@@ -169,3 +169,15 @@ class SettingsModel(BaseSettings):
 
 
 settings = SettingsModel.construct()
+
+
+def f(name: str) -> str:
+    return name
+
+
+class ModelWithAllowReuseValidator(BaseModel):
+    name: str
+    _normalize_name = validator('name', allow_reuse=True)(f)
+
+
+model_with_allow_reuse_validator = ModelWithAllowReuseValidator(name='xyz')
diff --git a/tests/mypy/modules/success.py b/tests/mypy/modules/success.py
index 51771d768..51611e373 100644
--- a/tests/mypy/modules/success.py
+++ b/tests/mypy/modules/success.py
@@ -42,6 +42,7 @@ from pydantic import (
     create_model,
     create_model_from_typeddict,
     root_validator,
+    stricturl,
     validate_arguments,
     validator,
 )
@@ -248,6 +249,10 @@ validated.my_file_path_str.absolute()
 validated.my_dir_path.absolute()
 validated.my_dir_path_str.absolute()
 
+stricturl(allowed_schemes={'http'})
+stricturl(allowed_schemes=frozenset({'http'}))
+stricturl(allowed_schemes=('s3', 's3n', 's3a'))
+
 
 class SomeDict(TypedDict):
     val: int
diff --git a/tests/mypy/outputs/plugin-success-strict.txt b/tests/mypy/outputs/plugin-success-strict.txt
index 8e61a4a54..80365a9df 100644
--- a/tests/mypy/outputs/plugin-success-strict.txt
+++ b/tests/mypy/outputs/plugin-success-strict.txt
@@ -1,4 +1,3 @@
 29: error: Unexpected keyword argument "z" for "Model"  [call-arg]
 64: error: Untyped fields disallowed  [pydantic-field]
-79: error: Argument "x" to "OverrideModel" has incompatible type "float"; expected "int"  [arg-type]
-125: error: Untyped fields disallowed  [pydantic-field]
\ No newline at end of file
+79: error: Argument "x" to "OverrideModel" has incompatible type "float"; expected "int"  [arg-type]
\ No newline at end of file
diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt
index efab4ad68..a6fece99c 100644
--- a/tests/requirements-linting.txt
+++ b/tests/requirements-linting.txt
@@ -1,10 +1,10 @@
-black==21.8b0
-flake8==3.9.2
-flake8-quotes==3.3.0
-hypothesis==6.18.0
-isort==5.9.3
+black==21.12b0
+flake8==4.0.1
+flake8-quotes==3.3.1
+hypothesis==6.30.1
+isort==5.10.1
 mypy==0.910
-types-toml==0.1.5
-pycodestyle==2.7.0
-pyflakes==2.3.1
-twine==3.4.2
\ No newline at end of file
+types-toml==0.10.1
+pycodestyle==2.8.0
+pyflakes==2.4.0
+twine==3.7.0
\ No newline at end of file
diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt
index 61855b531..cb76137e7 100644
--- a/tests/requirements-testing.txt
+++ b/tests/requirements-testing.txt
@@ -1,9 +1,9 @@
-coverage==5.5
-hypothesis==6.18.0
+coverage==6.2
+hypothesis==6.30.1
 # pin importlib-metadata as upper versions need typing-extensions to work if on python < 3.8
 importlib-metadata==3.1.0;python_version<"3.8"
 mypy==0.910
 pytest==6.2.5
-pytest-cov==2.12.1
+pytest-cov==3.0.0
 pytest-mock==3.6.1
 pytest-sugar==0.9.4
diff --git a/tests/test_annotated_types.py b/tests/test_annotated_types.py
index 4b4beab07..efff5348c 100644
--- a/tests/test_annotated_types.py
+++ b/tests/test_annotated_types.py
@@ -79,6 +79,8 @@ def test_namedtuple_schema():
                     {'title': 'X', 'type': 'integer'},
                     {'title': 'Y', 'type': 'integer'},
                 ],
+                'minItems': 2,
+                'maxItems': 2,
             },
             'pos2': {
                 'title': 'Pos2',
@@ -87,6 +89,8 @@ def test_namedtuple_schema():
                     {'title': 'X'},
                     {'title': 'Y'},
                 ],
+                'minItems': 2,
+                'maxItems': 2,
             },
             'pos3': {
                 'title': 'Pos3',
@@ -95,6 +99,8 @@ def test_namedtuple_schema():
                     {'type': 'integer'},
                     {'type': 'integer'},
                 ],
+                'minItems': 2,
+                'maxItems': 2,
             },
         },
         'required': ['pos1', 'pos2', 'pos3'],
diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py
index 95595fcbf..12a337f82 100644
--- a/tests/test_forward_ref.py
+++ b/tests/test_forward_ref.py
@@ -41,6 +41,57 @@ class Model(BaseModel):
     assert module.Model().dict() == {'a': None}
 
 
+@skip_pre_37
+def test_postponed_annotations_auto_update_forward_refs(create_module):
+    module = create_module(
+        # language=Python
+        """
+from __future__ import annotations
+from pydantic import BaseModel
+
+class Model(BaseModel):
+    a: Model
+"""
+    )
+
+    assert module.Model.__fields__['a'].type_ is module.Model
+
+
+def test_forward_ref_auto_update_no_model(create_module):
+    module = create_module(
+        # language=Python
+        """
+from pydantic import BaseModel
+
+class Foo(BaseModel):
+    a: 'Bar'
+
+class Bar(BaseModel):
+    b: 'Foo'
+"""
+    )
+
+    from pydantic.typing import ForwardRef
+
+    assert module.Foo.__fields__['a'].type_ == ForwardRef('Bar')
+    assert module.Bar.__fields__['b'].type_ is module.Foo
+
+
+def test_forward_ref_one_of_fields_not_defined(create_module):
+    @create_module
+    def module():
+        from pydantic import BaseModel
+
+        class Foo(BaseModel):
+            foo: 'Foo'
+            bar: 'Bar'  # noqa: F821
+
+    from pydantic.typing import ForwardRef
+
+    assert module.Foo.__fields__['bar'].type_ == ForwardRef('Bar')
+    assert module.Foo.__fields__['foo'].type_ is module.Foo
+
+
 def test_basic_forward_ref(create_module):
     @create_module
     def module():
@@ -509,13 +560,22 @@ def test_nested_forward_ref():
     class NestedTuple(BaseModel):
         x: Tuple[int, Optional['NestedTuple']]  # noqa: F821
 
-    with pytest.raises(ConfigError) as exc_info:
-        NestedTuple.parse_obj({'x': ('1', {'x': ('2', {'x': ('3', None)})})})
-    assert str(exc_info.value) == (
-        'field "x_1" not yet prepared so type is still a ForwardRef, '
-        'you might need to call NestedTuple.update_forward_refs().'
-    )
-
-    NestedTuple.update_forward_refs()
     obj = NestedTuple.parse_obj({'x': ('1', {'x': ('2', {'x': ('3', None)})})})
     assert obj.dict() == {'x': (1, {'x': (2, {'x': (3, None)})})}
+
+
+@skip_pre_37
+def test_class_var_as_string(create_module):
+    module = create_module(
+        # language=Python
+        """
+from __future__ import annotations
+from typing import ClassVar
+from pydantic import BaseModel
+
+class Model(BaseModel):
+    a: ClassVar[int]
+"""
+    )
+
+    assert module.Model.__class_vars__ == {'a'}
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 7e0cd731c..fb071b0cf 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -1160,6 +1160,92 @@ def test_generic_annotated():
     SomeGenericModel[str](the_alias='qwe')
 
 
+@skip_36
+def test_generic_subclass():
+    T = TypeVar('T')
+
+    class A(GenericModel, Generic[T]):
+        ...
+
+    class B(A[T], Generic[T]):
+        ...
+
+    assert B[int].__name__ == 'B[int]'
+    assert issubclass(B[int], B)
+    assert issubclass(B[int], A[int])
+    assert not issubclass(B[int], A[str])
+
+
+@skip_36
+def test_generic_subclass_with_partial_application():
+    T = TypeVar('T')
+    S = TypeVar('S')
+
+    class A(GenericModel, Generic[T]):
+        ...
+
+    class B(A[S], Generic[T, S]):
+        ...
+
+    PartiallyAppliedB = B[str, T]
+    assert issubclass(PartiallyAppliedB[int], A[int])
+    assert not issubclass(PartiallyAppliedB[int], A[str])
+    assert not issubclass(PartiallyAppliedB[str], A[int])
+
+
+@skip_36
+def test_multilevel_generic_binding():
+    T = TypeVar('T')
+    S = TypeVar('S')
+
+    class A(GenericModel, Generic[T, S]):
+        ...
+
+    class B(A[str, T], Generic[T]):
+        ...
+
+    assert B[int].__name__ == 'B[int]'
+    assert issubclass(B[int], A[str, int])
+    assert not issubclass(B[str], A[str, int])
+
+
+@skip_36
+def test_generic_subclass_with_extra_type():
+    T = TypeVar('T')
+    S = TypeVar('S')
+
+    class A(GenericModel, Generic[T]):
+        ...
+
+    class B(A[S], Generic[T, S]):
+        ...
+
+    assert B[int, str].__name__ == 'B[int, str]', B[int, str].__name__
+    assert issubclass(B[str, int], B)
+    assert issubclass(B[str, int], A[int])
+    assert not issubclass(B[int, str], A[int])
+
+
+@skip_36
+def test_multi_inheritance_generic_binding():
+    T = TypeVar('T')
+
+    class A(GenericModel, Generic[T]):
+        ...
+
+    class B(A[int], Generic[T]):
+        ...
+
+    class C(B[str], Generic[T]):
+        ...
+
+    assert C[float].__name__ == 'C[float]'
+    assert issubclass(C[float], B[str])
+    assert not issubclass(C[float], B[int])
+    assert issubclass(C[float], A[int])
+    assert not issubclass(C[float], A[str])
+
+
 @skip_36
 def test_parse_generic_json():
     T = TypeVar('T')
diff --git a/tests/test_json.py b/tests/test_json.py
index af974a164..9ef389283 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -7,7 +7,7 @@ from decimal import Decimal
 from enum import Enum
 from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
 from pathlib import Path
-from typing import List
+from typing import List, Optional
 from uuid import UUID
 
 import pytest
@@ -279,3 +279,43 @@ def test_custom_decode_encode():
     m = Model.parse_raw('${"a": 1, "b": "foo"}$$')
     assert m.dict() == {'a': 1, 'b': 'foo'}
     assert m.json() == '{\n  "a": 1,\n  "b": "foo"\n}'
+
+
+def test_json_nested_encode():
+    class Phone(BaseModel):
+        manufacturer: str
+        number: int
+
+    class User(BaseModel):
+        name: str
+        SSN: int
+        birthday: datetime.datetime
+        phone: Phone
+        friend: Optional['User'] = None  # noqa: F821  # https://github.com/PyCQA/pyflakes/issues/567
+
+        class Config:
+            json_encoders = {
+                datetime.datetime: lambda v: v.timestamp(),
+                Phone: lambda v: v.number if v else None,
+                'User': lambda v: v.SSN,
+            }
+
+    User.update_forward_refs()
+
+    iphone = Phone(manufacturer='Apple', number=18002752273)
+    galaxy = Phone(manufacturer='Samsung', number=18007267864)
+
+    timon = User(
+        name='Timon', SSN=123, birthday=datetime.datetime(1993, 6, 1, tzinfo=datetime.timezone.utc), phone=iphone
+    )
+    pumbaa = User(
+        name='Pumbaa', SSN=234, birthday=datetime.datetime(1993, 5, 15, tzinfo=datetime.timezone.utc), phone=galaxy
+    )
+
+    timon.friend = pumbaa
+
+    assert iphone.json() == '{"manufacturer": "Apple", "number": 18002752273}'
+    assert (
+        pumbaa.json() == '{"name": "Pumbaa", "SSN": 234, "birthday": 737424000.0, "phone": 18007267864, "friend": null}'
+    )
+    assert timon.json() == '{"name": "Timon", "SSN": 123, "birthday": 738892800.0, "phone": 18002752273, "friend": 234}'
diff --git a/tests/test_main.py b/tests/test_main.py
index 4a968104a..ceb925db4 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -2,7 +2,20 @@ import sys
 from collections import defaultdict
 from copy import deepcopy
 from enum import Enum
-from typing import Any, Callable, ClassVar, Counter, DefaultDict, Dict, List, Mapping, Optional, Type, get_type_hints
+from typing import (
+    Any,
+    Callable,
+    ClassVar,
+    Counter,
+    DefaultDict,
+    Dict,
+    List,
+    Mapping,
+    Optional,
+    Type,
+    TypeVar,
+    get_type_hints,
+)
 from uuid import UUID, uuid4
 
 import pytest
@@ -1976,6 +1989,27 @@ def test_typing_coercion_dict():
     assert repr(m) == "Model(x={'one': 1, 'two': 2})"
 
 
+@pytest.mark.skipif(sys.version_info < (3, 7), reason='generic classes need 3.7')
+def test_typing_non_coercion_of_dict_subclasses():
+    KT = TypeVar('KT')
+    VT = TypeVar('VT')
+
+    class MyDict(Dict[KT, VT]):
+        def __repr__(self):
+            return f'MyDict({super().__repr__()})'
+
+    class Model(BaseModel):
+        a: MyDict
+        b: MyDict[str, int]
+        c: Dict[str, int]
+        d: Mapping[str, int]
+
+    assert (
+        repr(Model(a=MyDict({'a': 1}), b=MyDict({'a': '1'}), c=MyDict({'a': '1'}), d=MyDict({'a': '1'})))
+        == "Model(a=MyDict({'a': 1}), b=MyDict({'a': 1}), c={'a': 1}, d=MyDict({'a': 1}))"
+    )
+
+
 def test_typing_coercion_defaultdict():
     class Model(BaseModel):
         x: DefaultDict[int, str]
diff --git a/tests/test_schema.py b/tests/test_schema.py
index cd4e2e4f8..709b468b2 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -29,7 +29,7 @@ from uuid import UUID
 import pytest
 from typing_extensions import Literal
 
-from pydantic import BaseModel, Extra, Field, ValidationError, conlist, conset, validator
+from pydantic import BaseModel, Extra, Field, ValidationError, confrozenset, conlist, conset, validator
 from pydantic.color import Color
 from pydantic.dataclasses import dataclass
 from pydantic.generics import GenericModel
@@ -398,6 +398,35 @@ def test_enum_and_model_have_same_behaviour():
     }
 
 
+def test_enum_includes_extra_without_other_params():
+    class Names(str, Enum):
+        rick = 'Rick'
+        morty = 'Morty'
+        summer = 'Summer'
+
+    class Foo(BaseModel):
+        enum: Names
+        extra_enum: Names = Field(..., extra='Extra field')
+
+    assert Foo.schema() == {
+        'definitions': {
+            'Names': {
+                'description': 'An enumeration.',
+                'enum': ['Rick', 'Morty', 'Summer'],
+                'title': 'Names',
+                'type': 'string',
+            },
+        },
+        'properties': {
+            'enum': {'$ref': '#/definitions/Names'},
+            'extra_enum': {'allOf': [{'$ref': '#/definitions/Names'}], 'extra': 'Extra field'},
+        },
+        'required': ['enum', 'extra_enum'],
+        'title': 'Foo',
+        'type': 'object',
+    }
+
+
 def test_list_enum_schema_extras():
     class FoodChoice(str, Enum):
         spam = 'spam'
@@ -535,34 +564,36 @@ def test_const_false():
 
 
 @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}),
     ],
 )
-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_deque():
@@ -1404,6 +1435,26 @@ def test_list_default():
     }
 
 
+def test_enum_str_default():
+    class MyEnum(str, Enum):
+        FOO = 'foo'
+
+    class UserModel(BaseModel):
+        friends: MyEnum = MyEnum.FOO
+
+    assert UserModel.schema()['properties']['friends']['default'] is MyEnum.FOO.value
+
+
+def test_enum_int_default():
+    class MyEnum(IntEnum):
+        FOO = 1
+
+    class UserModel(BaseModel):
+        friends: MyEnum = MyEnum.FOO
+
+    assert UserModel.schema()['properties']['friends']['default'] is MyEnum.FOO.value
+
+
 def test_dict_default():
     class UserModel(BaseModel):
         friends: Dict[str, float] = {'a': 1.1, 'b': 2.2}
@@ -1481,6 +1532,7 @@ def test_constraints_schema(kwargs, type_, expected_extra):
         ({'gt': 0}, Callable[[int], int]),
         ({'gt': 0}, conlist(int, min_items=4)),
         ({'gt': 0}, conset(int, min_items=4)),
+        ({'gt': 0}, confrozenset(int, min_items=4)),
     ],
 )
 def test_unenforced_constraints_schema(kwargs, type_):
@@ -1944,6 +1996,8 @@ def test_model_with_extra_forbidden():
                     {'exclusiveMinimum': 0, 'type': 'integer'},
                     {'exclusiveMinimum': 0, 'type': 'integer'},
                 ],
+                'minItems': 3,
+                'maxItems': 3,
             },
         ),
         (
@@ -2333,6 +2387,8 @@ def test_namedtuple_default():
                 'default': Coordinates(x=0, y=0),
                 'type': 'array',
                 'items': [{'title': 'X', 'type': 'number'}, {'title': 'Y', 'type': 'number'}],
+                'minItems': 2,
+                'maxItems': 2,
             }
         },
     }
@@ -2424,11 +2480,19 @@ def test_advanced_generic_schema():
                 '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'],
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 5fa2f549f..11f051993 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -830,6 +830,33 @@ def test_secrets_path_url(tmp_path):
     assert Settings().dict() == {'foo': 'http://www.example.com', 'bar': SecretStr('snap')}
 
 
+def test_secrets_path_json(tmp_path):
+    p = tmp_path / 'foo'
+    p.write_text('{"a": "b"}')
+
+    class Settings(BaseSettings):
+        foo: Dict[str, str]
+
+        class Config:
+            secrets_dir = tmp_path
+
+    assert Settings().dict() == {'foo': {'a': 'b'}}
+
+
+def test_secrets_path_invalid_json(tmp_path):
+    p = tmp_path / 'foo'
+    p.write_text('{"a": "b"')
+
+    class Settings(BaseSettings):
+        foo: Dict[str, str]
+
+        class Config:
+            secrets_dir = tmp_path
+
+    with pytest.raises(SettingsError, match='error parsing JSON for "foo"'):
+        Settings()
+
+
 def test_secrets_missing(tmp_path):
     class Settings(BaseSettings):
         foo: str
diff --git a/tests/test_types.py b/tests/test_types.py
index efb1bbe71..a0dddc0b4 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -22,6 +22,7 @@ from typing import (
     Sequence,
     Set,
     Tuple,
+    Union,
 )
 from uuid import UUID
 
@@ -64,6 +65,7 @@ from pydantic import (
     conbytes,
     condecimal,
     confloat,
+    confrozenset,
     conint,
     conlist,
     conset,
@@ -529,6 +531,97 @@ def test_conset_not_required():
     assert Model().foo is None
 
 
+def test_confrozenset():
+    class Model(BaseModel):
+        foo: FrozenSet[int] = Field(..., min_items=2, max_items=4)
+        bar: confrozenset(str, min_items=1, max_items=4) = None
+
+    m = Model(foo=[1, 2], bar=['spoon'])
+    assert m.dict() == {'foo': {1, 2}, 'bar': {'spoon'}}
+    assert isinstance(m.foo, frozenset)
+    assert isinstance(m.bar, frozenset)
+
+    assert Model(foo=[1, 1, 1, 2, 2], bar=['spoon']).dict() == {'foo': {1, 2}, 'bar': {'spoon'}}
+
+    with pytest.raises(ValidationError, match='ensure this value has at least 2 items'):
+        Model(foo=[1])
+
+    with pytest.raises(ValidationError, match='ensure this value has at most 4 items'):
+        Model(foo=list(range(5)))
+
+    assert Model.schema() == {
+        'title': 'Model',
+        'type': 'object',
+        'properties': {
+            'foo': {
+                'title': 'Foo',
+                'type': 'array',
+                'items': {'type': 'integer'},
+                'uniqueItems': True,
+                'minItems': 2,
+                'maxItems': 4,
+            },
+            'bar': {
+                'title': 'Bar',
+                'type': 'array',
+                'items': {'type': 'string'},
+                'uniqueItems': True,
+                'minItems': 1,
+                'maxItems': 4,
+            },
+        },
+        'required': ['foo'],
+    }
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(foo=[1, 'x', 'y'])
+    errors = exc_info.value.errors()
+    assert len(errors) == 2
+    assert all(error['msg'] == 'value is not a valid integer' for error in errors)
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(foo=1)
+    assert exc_info.value.errors() == [
+        {'loc': ('foo',), 'msg': 'value is not a valid frozenset', 'type': 'type_error.frozenset'}
+    ]
+
+
+def test_confrozenset_not_required():
+    class Model(BaseModel):
+        foo: Optional[FrozenSet[int]] = None
+
+    assert Model(foo=None).foo is None
+    assert Model().foo is None
+
+
+def test_constrained_frozenset_optional():
+    class Model(BaseModel):
+        req: Optional[confrozenset(str, min_items=1)] = ...
+        opt: Optional[confrozenset(str, min_items=1)]
+
+    assert Model(req=None).dict() == {'req': None, 'opt': None}
+    assert Model(req=None, opt=None).dict() == {'req': None, 'opt': None}
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(req=frozenset(), opt=frozenset())
+    assert exc_info.value.errors() == [
+        {
+            'loc': ('req',),
+            'msg': 'ensure this value has at least 1 items',
+            'type': 'value_error.frozenset.min_items',
+            'ctx': {'limit_value': 1},
+        },
+        {
+            'loc': ('opt',),
+            'msg': 'ensure this value has at least 1 items',
+            'type': 'value_error.frozenset.min_items',
+            'ctx': {'limit_value': 1},
+        },
+    ]
+
+    assert Model(req={'a'}, opt={'a'}).dict() == {'req': {'a'}, 'opt': {'a'}}
+
+
 class ConStringModel(BaseModel):
     v: constr(max_length=10) = 'foobar'
 
@@ -2838,6 +2931,143 @@ def test_none(value_type):
     ]
 
 
+def test_default_union_types():
+    class DefaultModel(BaseModel):
+        v: Union[int, bool, str]
+
+    assert DefaultModel(v=True).dict() == {'v': 1}
+    assert DefaultModel(v=1).dict() == {'v': 1}
+    assert DefaultModel(v='1').dict() == {'v': 1}
+
+    # In 3.6, Union[int, bool, str] == Union[int, str]
+    allowed_json_types = ('integer', 'string') if sys.version_info[:2] == (3, 6) else ('integer', 'boolean', 'string')
+
+    assert DefaultModel.schema() == {
+        'title': 'DefaultModel',
+        'type': 'object',
+        'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in allowed_json_types]}},
+        'required': ['v'],
+    }
+
+
+def test_smart_union_types():
+    class SmartModel(BaseModel):
+        v: Union[int, bool, str]
+
+        class Config:
+            smart_union = True
+
+    assert SmartModel(v=1).dict() == {'v': 1}
+    assert SmartModel(v=True).dict() == {'v': True}
+    assert SmartModel(v='1').dict() == {'v': '1'}
+
+    # In 3.6, Union[int, bool, str] == Union[int, str]
+    allowed_json_types = ('integer', 'string') if sys.version_info[:2] == (3, 6) else ('integer', 'boolean', 'string')
+
+    assert SmartModel.schema() == {
+        'title': 'SmartModel',
+        'type': 'object',
+        'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in allowed_json_types]}},
+        'required': ['v'],
+    }
+
+
+def test_default_union_class():
+    class A(BaseModel):
+        x: str
+
+    class B(BaseModel):
+        x: str
+
+    class Model(BaseModel):
+        y: Union[A, B]
+
+    assert isinstance(Model(y=A(x='a')).y, A)
+    # `B` instance is coerced to `A`
+    assert isinstance(Model(y=B(x='b')).y, A)
+
+
+def test_smart_union_class():
+    class A(BaseModel):
+        x: str
+
+    class B(BaseModel):
+        x: str
+
+    class Model(BaseModel):
+        y: Union[A, B]
+
+        class Config:
+            smart_union = True
+
+    assert isinstance(Model(y=A(x='a')).y, A)
+    assert isinstance(Model(y=B(x='b')).y, B)
+
+
+def test_default_union_subclass():
+    class MyStr(str):
+        ...
+
+    class Model(BaseModel):
+        x: Union[int, str]
+
+    assert Model(x=MyStr('1')).x == 1
+
+
+def test_smart_union_subclass():
+    class MyStr(str):
+        ...
+
+    class Model(BaseModel):
+        x: Union[int, str]
+
+        class Config:
+            smart_union = True
+
+    assert Model(x=MyStr('1')).x == '1'
+
+
+def test_default_union_compound_types():
+    class Model(BaseModel):
+        values: Union[Dict[str, str], List[str]]
+
+    assert Model(values={'L': '1'}).dict() == {'values': {'L': '1'}}
+    assert Model(values=['L1']).dict() == {'values': {'L': '1'}}  # dict(['L1']) == {'L': '1'}
+
+
+def test_smart_union_compound_types():
+    class Model(BaseModel):
+        values: Union[Dict[str, str], List[str], Dict[str, List[str]]]
+
+        class Config:
+            smart_union = True
+
+    assert Model(values={'L': '1'}).dict() == {'values': {'L': '1'}}
+    assert Model(values=['L1']).dict() == {'values': ['L1']}
+    assert Model(values=('L1',)).dict() == {'values': {'L': '1'}}  # expected coercion into first dict if not a list
+    assert Model(values={'x': ['pika']}) == {'values': {'x': ['pika']}}
+    assert Model(values={'x': ('pika',)}).dict() == {'values': {'x': ['pika']}}
+    with pytest.raises(ValidationError) as e:
+        Model(values={'x': {'a': 'b'}})
+    assert e.value.errors() == [
+        {'loc': ('values', 'x'), 'msg': 'str type expected', 'type': 'type_error.str'},
+        {'loc': ('values',), 'msg': 'value is not a valid list', 'type': 'type_error.list'},
+        {'loc': ('values', 'x'), 'msg': 'value is not a valid list', 'type': 'type_error.list'},
+    ]
+
+
+def test_smart_union_compouned_types_edge_case():
+    """For now, `smart_union` does not support well compound types"""
+
+    class Model(BaseModel, smart_union=True):
+        x: Union[List[str], List[int]]
+
+    # should consider [1, 2] valid and not coerce once `smart_union` is improved
+    assert Model(x=[1, 2]).x == ['1', '2']
+    # still coerce if needed
+    assert Model(x=[1, '2']).x == ['1', '2']
+
+
 @pytest.mark.parametrize(
     'value,result',
     (