Repository: samuelcolvin/pydantic
Pull request #2987 (2021-07-16 10:09:00)

Fix 2860
When dealing with JsonWrapper, replace the inner type when constructing
a a generic model.

## Change Summary

Fixes [bug 2860](https://github.com/samuelcolvin/pydantic/issues/2860)

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* N/A 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**


geekingfrog: please review

Patch:
diff --git a/changes/2860-geekingfrog.md b/changes/2860-geekingfrog.md
new file mode 100644
index 000000000..f12de66b3
--- /dev/null
+++ b/changes/2860-geekingfrog.md
@@ -0,0 +1,1 @@
+Correctly parse generic models with `Json[T]`.
diff --git a/pydantic/generics.py b/pydantic/generics.py
index c14da4e70..17d782feb 100644
--- a/pydantic/generics.py
+++ b/pydantic/generics.py
@@ -22,6 +22,7 @@ from typing_extensions import Annotated
 from .class_validators import gather_all_validators
 from .fields import DeferredType
 from .main import BaseModel, create_model
+from .types import JsonWrapper
 from .typing import display_as_type, get_all_type_hints, get_args, get_origin, typing_base
 from .utils import all_identical, lenient_issubclass
 
@@ -201,6 +202,12 @@ def replace_types(type_: Any, type_map: Mapping[Any, Any]) -> Any:
             return type_
         return resolved_list
 
+    # For JsonWrapperValue, need to handle its inner type to allow correct parsing
+    # of generic Json arguments like Json[T]
+    if not origin_type and lenient_issubclass(type_, JsonWrapper):
+        type_.inner_type = replace_types(type_.inner_type, type_map)
+        return type_
+
     # If all else fails, we try to resolve the type directly and otherwise just
     # return the input with no modifications.
     return type_map.get(type_, type_)


Test patch:
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 15ecc9d34..7e0cd731c 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -1,3 +1,4 @@
+import json
 import sys
 from enum import Enum
 from typing import (
@@ -19,7 +20,7 @@ from typing import (
 import pytest
 from typing_extensions import Annotated, Literal
 
-from pydantic import BaseModel, Field, ValidationError, root_validator, validator
+from pydantic import BaseModel, Field, Json, ValidationError, root_validator, validator
 from pydantic.generics import GenericModel, _generic_types_cache, iter_contained_typevars, replace_types
 
 skip_36 = pytest.mark.skipif(sys.version_info < (3, 7), reason='generics only supported for python 3.7 and above')
@@ -1157,3 +1158,27 @@ def test_generic_annotated():
         some_field: Annotated[T, Field(alias='the_alias')]
 
     SomeGenericModel[str](the_alias='qwe')
+
+
+@skip_36
+def test_parse_generic_json():
+    T = TypeVar('T')
+
+    class MessageWrapper(GenericModel, Generic[T]):
+        message: Json[T]
+
+    class Payload(BaseModel):
+        payload_field: str
+
+    raw = json.dumps({'payload_field': 'payload'})
+    record = MessageWrapper[Payload](message=raw)
+    assert isinstance(record.message, Payload)
+
+    schema = record.schema()
+    assert schema['properties'] == {'message': {'$ref': '#/definitions/Payload'}}
+    assert schema['definitions']['Payload'] == {
+        'title': 'Payload',
+        'type': 'object',
+        'properties': {'payload_field': {'title': 'Payload Field', 'type': 'string'}},
+        'required': ['payload_field'],
+    }