Repository: pydantic/pydantic

Issue:
JSON validation of model with SecretStr field fails
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

Hello,

During migration of our codebase to 2.4.2 we encountered the following bug. This was not a bug in 2.3.0!

JSON validation of a model with `SecretStr` field fails:

```python
from pydantic import BaseModel, SecretStr

class Model(BaseModel):
    foo:  SecretStr

model = Model.model_validate_json('{"foo": "a"}')
```

Fails with following error:

```
Traceback (most recent call last):
  File "/Users/tarren.peterson/Library/Application Support/JetBrains/PyCharm2023.2/scratches/pydantic_scratch/pydantic_bug.py", line 6, in <module>
    model = Model.model_validate_json('{"foo": "a"}')
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/tarren.peterson/Library/Caches/pypoetry/virtualenvs/ea-forecasting-tools-D5CGYiI1-py3.11/lib/python3.11/site-packages/pydantic/main.py", line 530, in model_validate_json
    return cls.__pydantic_validator__.validate_json(json_data, strict=strict, context=context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
NotImplementedError: Cannot check isinstance when validating from json, use a JsonOrPython validator instead.
```



### Python, Pydantic & OS Version

```Text
pydantic version: 2.4.2
pydantic-core version: 2.10.1
pydantic-core build: profile=release pgo=false
install path: /Users/tarren.peterson/Library/Caches/pypoetry/virtualenvs/ea-forecasting-tools-D5CGYiI1-py3.11/lib/python3.11/site-packages/pydantic
python version: 3.11.1 (main, Aug 24 2023, 07:39:00) [Clang 14.0.3 (clang-1403.0.22.14.1)]
platform: macOS-13.5.1-arm64-arm-64bit
related packages: fastapi-0.103.0 typing_extensions-4.8.0 pydantic-settings-2.0.3
```



Discussion:
Hi @terranjp,

Thanks for reporting this! This does look like a bug, and we'll work on a fix for v2.4.3.

Resolving patch:
diff --git a/pydantic/types.py b/pydantic/types.py
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -1448,16 +1448,20 @@ def get_json_schema(_core_schema: core_schema.CoreSchema, handler: GetJsonSchema
             )
             return json_schema
 
-        s = core_schema.union_schema(
-            [
-                core_schema.is_instance_schema(source),
-                core_schema.no_info_after_validator_function(
-                    source,  # construct the type
-                    inner_schema,
-                ),
-            ],
-            strict=True,
-            custom_error_type=error_kind,
+        json_schema = core_schema.no_info_after_validator_function(
+            source,  # construct the type
+            inner_schema,
+        )
+        s = core_schema.json_or_python_schema(
+            python_schema=core_schema.union_schema(
+                [
+                    core_schema.is_instance_schema(source),
+                    json_schema,
+                ],
+                strict=True,
+                custom_error_type=error_kind,
+            ),
+            json_schema=json_schema,
             serialization=core_schema.plain_serializer_function_ser_schema(
                 serialize,
                 info_arg=True,


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -3921,13 +3921,25 @@ class Foobar(BaseModel):
     ]
 
 
-def test_secretstr():
+@pytest.mark.parametrize('validate_json', [True, False])
+def test_secretstr(validate_json):
     class Foobar(BaseModel):
         password: SecretStr
         empty_password: SecretStr
 
-    # Initialize the model.
-    f = Foobar(password='1234', empty_password='')
+    if validate_json:
+        f = Foobar.model_validate_json('{"password": "1234", "empty_password": ""}')
+        with pytest.raises(ValidationError) as exc_info:
+            Foobar.model_validate_json('{"password": 1234, "empty_password": null}')
+    else:
+        f = Foobar(password='1234', empty_password='')
+        with pytest.raises(ValidationError) as exc_info:
+            Foobar(password=1234, empty_password=None)
+
+    assert exc_info.value.errors(include_url=False) == [
+        {'type': 'string_type', 'loc': ('password',), 'msg': 'Input should be a valid string', 'input': 1234},
+        {'type': 'string_type', 'loc': ('empty_password',), 'msg': 'Input should be a valid string', 'input': None},
+    ]
 
     # Assert correct types.
     assert f.password.__class__.__name__ == 'SecretStr'