Repository: samuelcolvin/pydantic (python)

Issue:
V1.9 Name Collisions JSON model_as_dict
### Checks

* [ ] I added a descriptive title to this issue
* [ ] I have searched (google, github) for similar issues and couldn't find anything
* [ ] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

In 1.9 Code changed in this merge #2650 The "models_as_dict" option for json has collision issues when the class names are the same but in multiple files. This is because of the change to __name__ in custom_pydantic_encoder. In the following code this will result in an error as DBUser.User does not have the field Address. If we force the encoder to be defined after the class this issue does not happen. The other option would be modification of update_forward_refs but that seems a lot worse. 

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
  pydantic version: 1.9.0a2
  pydantic compiled: True
  install path: /PycharmProjects/pythonProject/venv/lib/python3.8/site-packages/pydantic
  python version: 3.8.9 (default, Oct 26 2021, 07:25:53)  [Clang 13.0.0 (clang-1300.0.29.30)]
 
 Tried it in 3.10 as well. 
 ```
<!-- or if you're using pydantic prior to v1.3, manually include: OS, python version and pydantic version -->

<!-- Please read the [docs](https://pydantic-docs.helpmanual.io/) and search through issues to
confirm your bug hasn't already been reported. -->

<!-- Where possible please include a self-contained code snippet describing your bug: -->

```py
##DBusers.py
from pydantic import BaseModel
from uuid import UUID

class User(BaseModel):
    name: str
    user_id: UUID


```


```py
##this one throws Exception
from typing import List, Optional

from pydantic import BaseModel
from DBUser import User as DBUser
from uuid import uuid4

class Address(BaseModel):
    city: str
    country: str


class User(BaseModel):
    name: str
    address: Address
    friends: Optional[List['User']] = None
    connections: Optional[List[DBUser]]=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')),
    ],
    connections=[
        DBUser(name="joe",user_id=uuid4()),
        DBUser(name="Jeff", user_id=uuid4()),
    ]
)
wolfgang.json(indent=2,models_as_dict=False) ##Raises AttributeError: 'User' object has no attribute 'address'


```

```py
##this one does not throw Exception

from typing import List, Optional
from pydantic import BaseModel
from DBUser import User as DBUser
from uuid import uuid4

class Address(BaseModel):
    city: str
    country: str


class User(BaseModel):
    name: str
    address: Address
    friends: Optional[List["User"]] = None
    connections: Optional[List[DBUser]]=None

    class Config:
        json_encoders = {
            Address: lambda a: f'{a.city} ({a.country})',
        }


User.update_forward_refs()

User.Config.json_encoders[User] =lambda u: f'{u.name} in {u.address.city} ' f'({u.address.country[:2].upper()})'


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')),
    ],
    connections=[
        DBUser(name="joe",user_id=uuid4()),
        DBUser(name="Jeff", user_id=uuid4()),
    ]
)
print(wolfgang.json(indent=2,models_as_dict=False)) ##Does not explode


```


Pull request description:
apply `update_forward_refs` to `json_encoders`
## Change Summary

Apply `update_forward_refs` to `Config.json_encodes` prevent name clashes in types defined via strings.

## Related issue number

fix #3583

## 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)
* [ ] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 66717095d..c33c64d3a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -138,7 +138,7 @@ jobs:
       COMPILED: no
       DEPS: yes
 
-    runs-on: ${{ format('{0}-latest', matrix.os) }}
+    runs-on: ${{ matrix.os }}-latest
     steps:
     - uses: actions/checkout@v2
 
@@ -164,8 +164,49 @@ jobs:
         name: coverage
         path: coverage
 
+  test-old-mypy:
+    name: test mypy v${{ matrix.mypy-version }}
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        mypy-version: ['0.910', '0.920', '0.921']
+
+    steps:
+    - uses: actions/checkout@v2
+
+    - name: set up python
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.10'
+
+    - name: install
+      run: |
+        make install-testing
+        pip freeze
+
+    - name: uninstall deps
+      run: pip uninstall -y mypy tomli toml
+
+    - name: install specific mypy version
+      run: pip install mypy==${{ matrix.mypy-version }}
+
+    - run: mkdir coverage
+
+    - name: run tests
+      run: pytest --cov=pydantic tests/mypy
+      env:
+        COVERAGE_FILE: coverage/.coverage.linux-py3.10-mypy${{ matrix.mypy-version }}
+        CONTEXT: linux-py3.10-mypy${{ matrix.mypy-version }}
+
+    - name: store coverage files
+      uses: actions/upload-artifact@v2
+      with:
+        name: coverage
+        path: coverage
+
   coverage-combine:
-    needs: [test-linux, test-windows-mac]
+    needs: [test-linux, test-windows-mac, test-old-mypy]
     runs-on: ubuntu-latest
 
     steps:
@@ -236,7 +277,7 @@ jobs:
 
   build:
     name: build py3.${{ matrix.python-version }} on ${{ matrix.platform || matrix.os }}
-    needs: [lint, test-linux, test-windows-mac, test-fastapi, benchmark]
+    needs: [lint, test-linux, test-windows-mac, test-old-mypy, test-fastapi, benchmark]
     if: "success() && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master')"
     strategy:
       fail-fast: false
@@ -249,7 +290,7 @@ jobs:
         - os: windows
           ls: dir
 
-    runs-on: ${{ format('{0}-latest', matrix.os) }}
+    runs-on: ${{ matrix.os }}-latest
     steps:
     - uses: actions/checkout@v2
 
diff --git a/changes/3583-samuelcolvin.md b/changes/3583-samuelcolvin.md
new file mode 100644
index 000000000..c8c3a6271
--- /dev/null
+++ b/changes/3583-samuelcolvin.md
@@ -0,0 +1,1 @@
+Apply `update_forward_refs` to `Config.json_encodes` prevent name clashes in types defined via strings.
diff --git a/docs/mypy_plugin.md b/docs/mypy_plugin.md
index 437454b4e..566f625ae 100644
--- a/docs/mypy_plugin.md
+++ b/docs/mypy_plugin.md
@@ -86,6 +86,8 @@ To get started, all you need to do is create a `mypy.ini` file with following co
 plugins = pydantic.mypy
 ```
 
+The plugin is compatible with mypy versions 0.910, 0.920, 0.921 and 0.930.
+
 See the [mypy usage](usage/mypy.md) and [plugin configuration](#configuring-the-plugin) docs for more details.
 
 ### Plugin Settings
diff --git a/pydantic/class_validators.py b/pydantic/class_validators.py
index b6a26dab3..ecaeef393 100644
--- a/pydantic/class_validators.py
+++ b/pydantic/class_validators.py
@@ -9,6 +9,9 @@ from .errors import ConfigError
 from .typing import AnyCallable
 from .utils import ROOT_KEY, in_ipython
 
+if TYPE_CHECKING:
+    from .typing import AnyClassMethod
+
 
 class Validator:
     __slots__ = 'func', 'pre', 'each_item', 'always', 'check_fields', 'skip_on_failure'
@@ -54,7 +57,7 @@ def validator(
     check_fields: bool = True,
     whole: bool = None,
     allow_reuse: bool = False,
-) -> Callable[[AnyCallable], classmethod]:  # type: ignore[type-arg]
+) -> Callable[[AnyCallable], 'AnyClassMethod']:
     """
     Decorate methods on the class indicating that they should be used to validate fields
     :param fields: which field(s) the method should be called on
@@ -81,7 +84,7 @@ def validator(
         assert each_item is False, '"each_item" and "whole" conflict, remove "whole"'
         each_item = not whole
 
-    def dec(f: AnyCallable) -> classmethod:  # type: ignore[type-arg]
+    def dec(f: AnyCallable) -> 'AnyClassMethod':
         f_cls = _prepare_validator(f, allow_reuse)
         setattr(
             f_cls,
@@ -97,20 +100,20 @@ def validator(
 
 
 @overload
-def root_validator(_func: AnyCallable) -> classmethod:  # type: ignore[type-arg]
+def root_validator(_func: AnyCallable) -> 'AnyClassMethod':
     ...
 
 
 @overload
 def root_validator(
     *, pre: bool = False, allow_reuse: bool = False, skip_on_failure: bool = False
-) -> Callable[[AnyCallable], classmethod]:  # type: ignore[type-arg]
+) -> Callable[[AnyCallable], 'AnyClassMethod']:
     ...
 
 
 def root_validator(
     _func: Optional[AnyCallable] = None, *, pre: bool = False, allow_reuse: bool = False, skip_on_failure: bool = False
-) -> Union[classmethod, Callable[[AnyCallable], classmethod]]:  # type: ignore[type-arg]
+) -> Union['AnyClassMethod', Callable[[AnyCallable], 'AnyClassMethod']]:
     """
     Decorate methods on a model indicating that they should be used to validate (and perhaps modify) data either
     before or after standard model parsing/validation is performed.
@@ -122,7 +125,7 @@ def root_validator(
         )
         return f_cls
 
-    def dec(f: AnyCallable) -> classmethod:  # type: ignore[type-arg]
+    def dec(f: AnyCallable) -> 'AnyClassMethod':
         f_cls = _prepare_validator(f, allow_reuse)
         setattr(
             f_cls, ROOT_VALIDATOR_CONFIG_KEY, Validator(func=f_cls.__func__, pre=pre, skip_on_failure=skip_on_failure)
@@ -132,7 +135,7 @@ def root_validator(
     return dec
 
 
-def _prepare_validator(function: AnyCallable, allow_reuse: bool) -> classmethod:  # type: ignore[type-arg]
+def _prepare_validator(function: AnyCallable, allow_reuse: bool) -> 'AnyClassMethod':
     """
     Avoid validators with duplicated names since without this, validators can be overwritten silently
     which generally isn't the intended behaviour, don't run in ipython (see #312) or if allow_reuse is False.
@@ -325,7 +328,7 @@ def _generic_validator_basic(validator: AnyCallable, sig: 'Signature', args: Set
         return lambda cls, v, values, field, config: validator(v, values=values, field=field, config=config)
 
 
-def gather_all_validators(type_: 'ModelOrDc') -> Dict[str, classmethod]:  # type: ignore[type-arg]
+def gather_all_validators(type_: 'ModelOrDc') -> Dict[str, 'AnyClassMethod']:
     all_attributes = ChainMap(*[cls.__dict__ for cls in type_.__mro__])
     return {
         k: v
diff --git a/pydantic/config.py b/pydantic/config.py
index 646e60ca8..b37cd98ff 100644
--- a/pydantic/config.py
+++ b/pydantic/config.py
@@ -59,7 +59,8 @@ class BaseConfig:
     schema_extra: Union[Dict[str, Any], 'SchemaExtraCallable'] = {}
     json_loads: Callable[[str], Any] = json.loads
     json_dumps: Callable[..., str] = json.dumps
-    json_encoders: Dict[Type[Any], AnyCallable] = {}
+    # key type should include ForwardRef, but that breaks with python3.6
+    json_encoders: Dict[Union[Type[Any], str], AnyCallable] = {}
     underscore_attrs_are_private: bool = False
 
     # whether inherited models as fields should be reconstructed as base model
diff --git a/pydantic/json.py b/pydantic/json.py
index d03f3b042..ce956fea2 100644
--- a/pydantic/json.py
+++ b/pydantic/json.py
@@ -103,10 +103,7 @@ def custom_pydantic_encoder(type_encoders: Dict[Any, Callable[[Type[Any]], Any]]
         try:
             encoder = type_encoders[base]
         except KeyError:
-            try:
-                encoder = type_encoders[base.__name__]
-            except KeyError:
-                continue
+            continue
 
         return encoder(obj)
     else:  # We have exited the for loop without finding a suitable encoder
diff --git a/pydantic/main.py b/pydantic/main.py
index ab5fb42a4..eea8abcbb 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -66,6 +66,7 @@ if TYPE_CHECKING:
     from .types import ModelOrDc
     from .typing import (
         AbstractSetIntStr,
+        AnyClassMethod,
         CallableGenerator,
         DictAny,
         DictStrAny,
@@ -769,14 +770,14 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
         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,))
+        update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, {}, (NameError,))
 
     @classmethod
     def update_forward_refs(cls, **localns: Any) -> None:
         """
         Try to update ForwardRefs on fields based on this Model, globalns and localns.
         """
-        update_model_forward_refs(cls, cls.__fields__.values(), localns)
+        update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, localns)
 
     def __iter__(self) -> 'TupleGenerator':
         """
@@ -890,7 +891,7 @@ def create_model(
     __config__: Optional[Type[BaseConfig]] = None,
     __base__: None = None,
     __module__: str = __name__,
-    __validators__: Dict[str, classmethod] = None,  # type: ignore[type-arg]
+    __validators__: Dict[str, 'AnyClassMethod'] = None,
     **field_definitions: Any,
 ) -> Type['BaseModel']:
     ...
@@ -903,7 +904,7 @@ def create_model(
     __config__: Optional[Type[BaseConfig]] = None,
     __base__: Union[Type['Model'], Tuple[Type['Model'], ...]],
     __module__: str = __name__,
-    __validators__: Dict[str, classmethod] = None,  # type: ignore[type-arg]
+    __validators__: Dict[str, 'AnyClassMethod'] = None,
     **field_definitions: Any,
 ) -> Type['Model']:
     ...
@@ -915,7 +916,7 @@ def create_model(
     __config__: Optional[Type[BaseConfig]] = None,
     __base__: Union[None, Type['Model'], Tuple[Type['Model'], ...]] = None,
     __module__: str = __name__,
-    __validators__: Dict[str, classmethod] = None,  # type: ignore[type-arg]
+    __validators__: Dict[str, 'AnyClassMethod'] = None,
     **field_definitions: Any,
 ) -> Type['Model']:
     """
diff --git a/pydantic/mypy.py b/pydantic/mypy.py
index e7b7996a7..d89fe0436 100644
--- a/pydantic/mypy.py
+++ b/pydantic/mypy.py
@@ -1,20 +1,6 @@
 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
-    # future-proofing for upcoming `mypy` releases which will switch dependencies
-    try:
-        import tomli as toml  # type: ignore
-    except ImportError:
-        import warnings
-
-        warnings.warn('No TOML parser installed, cannot read configuration from `pyproject.toml`.')
-        toml = None  # type: ignore
-
 from mypy.errorcodes import ErrorCode
 from mypy.nodes import (
     ARG_NAMED,
@@ -60,13 +46,21 @@ from mypy.types import (
     Type,
     TypeOfAny,
     TypeType,
-    TypeVarLikeType,
     TypeVarType,
     UnionType,
     get_proper_type,
 )
 from mypy.typevars import fill_typevars
 from mypy.util import get_unique_redefinition_name
+from mypy.version import __version__ as mypy_version
+
+from pydantic.utils import is_valid_field
+
+try:
+    from mypy.types import TypeVarDef  # type: ignore[attr-defined]
+except ImportError:  # pragma: no cover
+    # Backward-compatible with TypeVarDef from Mypy 0.910.
+    from mypy.types import TypeVarType as TypeVarDef
 
 CONFIGFILE_KEY = 'pydantic-mypy'
 METADATA_KEY = 'pydantic-mypy-metadata'
@@ -74,6 +68,7 @@ BASEMODEL_FULLNAME = 'pydantic.main.BaseModel'
 BASESETTINGS_FULLNAME = 'pydantic.env_settings.BaseSettings'
 FIELD_FULLNAME = 'pydantic.fields.Field'
 DATACLASS_FULLNAME = 'pydantic.dataclasses.dataclass'
+BUILTINS_NAME = 'builtins' if float(mypy_version) >= 0.930 else '__builtins__'
 
 
 def plugin(version: str) -> 'TypingType[Plugin]':
@@ -125,9 +120,9 @@ class PydanticPluginConfig:
         if options.config_file is None:  # pragma: no cover
             return
 
-        if toml and options.config_file.endswith('.toml'):
-            with open(options.config_file, 'r') as rf:
-                config = toml.load(rf).get('tool', {}).get('pydantic-mypy', {})
+        toml_config = parse_toml(options.config_file)
+        if toml_config is not None:
+            config = toml_config.get('tool', {}).get('pydantic-mypy', {})
             for key in self.__slots__:
                 setting = config.get(key, False)
                 if not isinstance(setting, bool):
@@ -348,26 +343,32 @@ class PydanticModelTransformer:
         and does not treat settings fields as optional.
         """
         ctx = self._ctx
-        set_str = ctx.api.named_type('builtins.set', [ctx.api.named_type('builtins.str')])
+        set_str = ctx.api.named_type(f'{BUILTINS_NAME}.set', [ctx.api.named_type(f'{BUILTINS_NAME}.str')])
         optional_set_str = UnionType([set_str, NoneType()])
         fields_set_argument = Argument(Var('_fields_set', optional_set_str), optional_set_str, None, ARG_OPT)
         construct_arguments = self.get_field_arguments(fields, typed=True, force_all_optional=False, use_alias=False)
         construct_arguments = [fields_set_argument] + construct_arguments
 
-        obj_type = ctx.api.named_type('builtins.object')
+        obj_type = ctx.api.named_type(f'{BUILTINS_NAME}.object')
         self_tvar_name = '_PydanticBaseModel'  # Make sure it does not conflict with other names in the class
         tvar_fullname = ctx.cls.fullname + '.' + self_tvar_name
-        self_type = TypeVarType(self_tvar_name, tvar_fullname, -1, [], obj_type)
+        tvd = TypeVarDef(self_tvar_name, tvar_fullname, -1, [], obj_type)
         self_tvar_expr = TypeVarExpr(self_tvar_name, tvar_fullname, [], obj_type)
         ctx.cls.info.names[self_tvar_name] = SymbolTableNode(MDEF, self_tvar_expr)
 
+        # Backward-compatible with TypeVarDef from Mypy 0.910.
+        if isinstance(tvd, TypeVarType):
+            self_type = tvd
+        else:
+            self_type = TypeVarType(tvd)  # type: ignore[call-arg]
+
         add_method(
             ctx,
             'construct',
             construct_arguments,
             return_type=self_type,
             self_type=self_type,
-            tvar_like_type=self_type,
+            tvar_def=tvd,
             is_classmethod=True,
         )
 
@@ -619,7 +620,7 @@ def add_method(
     args: List[Argument],
     return_type: Type,
     self_type: Optional[Type] = None,
-    tvar_like_type: Optional[TypeVarLikeType] = None,
+    tvar_def: Optional[TypeVarDef] = None,
     is_classmethod: bool = False,
     is_new: bool = False,
     # is_staticmethod: bool = False,
@@ -654,10 +655,10 @@ def add_method(
         arg_names.append(get_name(arg.variable))
         arg_kinds.append(arg.kind)
 
-    function_type = ctx.api.named_type('builtins.function')
+    function_type = ctx.api.named_type(f'{BUILTINS_NAME}.function')
     signature = CallableType(arg_types, arg_kinds, arg_names, return_type, function_type)
-    if tvar_like_type:
-        signature.variables = [tvar_like_type]
+    if tvar_def:
+        signature.variables = [tvar_def]
 
     func = FuncDef(name, args, Block([PassStmt()]))
     func.info = info
@@ -714,3 +715,25 @@ def get_name(x: Union[FuncBase, SymbolNode]) -> str:
     if callable(fn):  # pragma: no cover
         return fn()
     return fn
+
+
+def parse_toml(config_file: str) -> Optional[Dict[str, Any]]:
+    if not config_file.endswith('.toml'):
+        return None
+
+    read_mode = 'rb'
+    try:
+        import tomli as toml_
+    except ImportError:
+        # older versions of mypy have toml as a dependency, not tomli
+        read_mode = 'r'
+        try:
+            import toml as toml_  # type: ignore[no-redef]
+        except ImportError:  # pragma: no cover
+            import warnings
+
+            warnings.warn('No TOML parser installed, cannot read configuration from `pyproject.toml`.')
+            return None
+
+    with open(config_file, read_mode) as rf:
+        return toml_.load(rf)  # type: ignore[arg-type]
diff --git a/pydantic/typing.py b/pydantic/typing.py
index c00d504aa..730dc4644 100644
--- a/pydantic/typing.py
+++ b/pydantic/typing.py
@@ -228,6 +228,7 @@ if TYPE_CHECKING:
     MappingIntStrAny = Mapping[IntStr, Any]
     CallableGenerator = Generator[AnyCallable, None, None]
     ReprArgs = Sequence[Tuple[Optional[str], Any]]
+    AnyClassMethod = classmethod[Any]
 
 __all__ = (
     'ForwardRef',
@@ -258,6 +259,7 @@ __all__ = (
     'DictIntStrAny',
     'CallableGenerator',
     'ReprArgs',
+    'AnyClassMethod',
     'CallableGenerator',
     'WithArgsTypes',
     'get_args',
@@ -459,6 +461,7 @@ def update_field_forward_refs(field: 'ModelField', globalns: Any, localns: Any)
 def update_model_forward_refs(
     model: Type[Any],
     fields: Iterable['ModelField'],
+    json_encoders: Dict[Union[Type[Any], str], AnyCallable],
     localns: 'DictStrAny',
     exc_to_suppress: Tuple[Type[BaseException], ...] = (),
 ) -> None:
@@ -478,6 +481,21 @@ def update_model_forward_refs(
         except exc_to_suppress:
             pass
 
+    for key in set(json_encoders.keys()):
+        if isinstance(key, str):
+            fr: ForwardRef = ForwardRef(key)
+        elif isinstance(key, ForwardRef):
+            fr = key
+        else:
+            continue
+
+        try:
+            new_key = evaluate_forwardref(fr, globalns, localns or None)
+        except exc_to_suppress:  # pragma: no cover
+            continue
+
+        json_encoders[new_key] = json_encoders.pop(key)
+
 
 def get_class(type_: Type[Any]) -> Union[None, bool, Type[Any]]:
     """
diff --git a/pydantic/utils.py b/pydantic/utils.py
index 31ff5dc64..2a3960f6d 100644
--- a/pydantic/utils.py
+++ b/pydantic/utils.py
@@ -574,7 +574,8 @@ class ValueItems(Representation):
         elif isinstance(items, AbstractSet):
             items = dict.fromkeys(items, ...)
         else:
-            raise TypeError(f'Unexpected type of exclude value {items.__class__}')  # type: ignore[attr-defined]
+            class_name = getattr(items, '__class__', '???')
+            raise TypeError(f'Unexpected type of exclude value {class_name}')
         return items
 
     @classmethod
diff --git a/setup.cfg b/setup.cfg
index ba05e3047..2fdb68e91 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -76,3 +76,6 @@ ignore_missing_imports = true
 
 [mypy-dotenv]
 ignore_missing_imports = true
+
+[mypy-toml]
+ignore_missing_imports = true


Test patch:
diff --git a/tests/mypy/test_mypy.py b/tests/mypy/test_mypy.py
index ea472ba98..5f772eb72 100644
--- a/tests/mypy/test_mypy.py
+++ b/tests/mypy/test_mypy.py
@@ -7,8 +7,10 @@ import pytest
 
 try:
     from mypy import api as mypy_api
+    from mypy.version import __version__ as mypy_version
 except ImportError:
     mypy_api = None  # type: ignore
+    mypy_version = '0'
 
 try:
     import dotenv
@@ -72,6 +74,15 @@ def test_mypy_results(config_filename: str, python_filename: str, output_filenam
         raise RuntimeError(f'wrote actual output to {output_path} since file did not exist')
 
     expected_out = Path(output_path).read_text() if output_path else ''
+
+    # fix for compatibility between mypy versions: (this can be dropped once we drop support for mypy<0.930)
+    if actual_out and float(mypy_version) < 0.930:
+        actual_out = actual_out.lower()
+        expected_out = expected_out.lower()
+        actual_out = actual_out.replace('variant:', 'variants:')
+        actual_out = re.sub(r'^(\d+: note: {4}).*', r'\1...', actual_out, flags=re.M)
+        expected_out = re.sub(r'^(\d+: note: {4}).*', r'\1...', expected_out, flags=re.M)
+
     assert actual_out == expected_out, actual_out
 
 
diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt
index 263524bc2..9a3f0464b 100644
--- a/tests/requirements-linting.txt
+++ b/tests/requirements-linting.txt
@@ -8,4 +8,3 @@ pre-commit==2.16.0
 pycodestyle==2.8.0
 pyflakes==2.4.0
 twine==3.7.1
-types-toml==0.10.1
diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py
index aac1dae10..df378810c 100644
--- a/tests/test_forward_ref.py
+++ b/tests/test_forward_ref.py
@@ -626,3 +626,60 @@ class Model(BaseModel):
     )
 
     assert module.Model.__class_vars__ == {'a'}
+
+
+@skip_pre_37
+def test_json_encoder_str(create_module):
+    module = create_module(
+        # language=Python
+        """
+from pydantic import BaseModel
+
+
+class User(BaseModel):
+    x: str
+
+
+FooUser = User
+
+
+class User(BaseModel):
+    y: str
+
+
+class Model(BaseModel):
+    foo_user: FooUser
+    user: User
+
+    class Config:
+        json_encoders = {
+            'User': lambda v: f'User({v.y})',
+        }
+"""
+    )
+
+    m = module.Model(foo_user={'x': 'user1'}, user={'y': 'user2'})
+    assert m.json(models_as_dict=False) == '{"foo_user": {"x": "user1"}, "user": "User(user2)"}'
+
+
+@skip_pre_37
+def test_json_encoder_forward_ref(create_module):
+    module = create_module(
+        # language=Python
+        """
+from pydantic import BaseModel
+from typing import ForwardRef, List, Optional
+
+class User(BaseModel):
+    name: str
+    friends: Optional[List['User']] = None
+
+    class Config:
+        json_encoders = {
+            ForwardRef('User'): lambda v: f'User({v.name})',
+        }
+"""
+    )
+
+    m = module.User(name='anne', friends=[{'name': 'ben'}, {'name': 'charlie'}])
+    assert m.json(models_as_dict=False) == '{"name": "anne", "friends": ["User(ben)", "User(charlie)"]}'