Repository: pydantic/pydantic

Issue:
Subclass of SecretStr expects `bytes` as input
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

For my use-case I have defined a subclass of the `SecretStr` type. This functionality worked fine in pydantic V1. Since the migration to V2 the follow error appeared continuously:

```
pydantic_core._pydantic_core.ValidationError: 1 validation error for [redacted]
shared_secret
  Input should be a valid bytes [type=bytes_type, input_value='[redacted]', input_type=str]
```

While the type is defined as follows:

```
class EncryptedBase64Str(SecretStr):
    def get_decrypted_value(self, aws_key_id: Union[str, UUID], **kwargs) -> str:
        decrypt_with_key(self.get_secret_value(), key=aws_key_id)
```

I.e. it is counter intuitive that the expected type is "bytes" when the class inherits from the specialized str variant.

This is due to the too restrictive test in `pydantic.types._SecretFieldValidator.__get_pydantic_core_schema__` where type is evaluated based on the types defined by pydantic:

```
        self.inner_schema = handler(str if self.field_type is SecretStr else bytes)
        error_kind = 'string_type' if self.field_type is SecretStr else 'bytes_type'
```

Children of SecretStr are neglected here.


### Example Code

```Python
from pydantic import SecretStr, ValidationError
from pydantic_settings import BaseSettings
from typing import Union
from uuid import UUID


class EncryptedBase64Str(SecretStr):
    def get_decrypted_value(self, aws_key_id: Union[str, UUID], **kwargs) -> str:
        # simplified for demonstration purposes
        return self.get_secret_value()


class Settings(BaseSettings):
    some_decryptable_secret: EncryptedBase64Str

try:
    Settings(some_decryptable_secret="abc")
except ValidationError as error:
    print(error)
    exit(0)

raise Exception("bug is not reproducible")
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.0.3
        pydantic-core version: 2.3.0 release build profile
                 install path: /Users/alexandervandenbulcke/Library/Caches/pypoetry/virtualenvs/jarvis-api-5-3ao05Y-py3.9/lib/python3.9/site-packages/pydantic
               python version: 3.9.16 (main, Dec  7 2022, 10:02:13)  [Clang 14.0.0 (clang-1400.0.29.202)]
                     platform: macOS-12.5.1-arm64-arm-64bit
     optional deps. installed: ['typing-extensions']
```


Selected Assignee: @samuelcolvin


Discussion:

Resolving patch:
diff --git a/pydantic/types.py b/pydantic/types.py
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -728,7 +728,7 @@ class _SecretFieldValidator:
     inner_schema: CoreSchema = _dataclasses.field(init=False)
 
     def validate(self, value: _SecretField[SecretType] | SecretType, _: core_schema.ValidationInfo) -> Any:
-        error_prefix: Literal['string', 'bytes'] = 'string' if self.field_type is SecretStr else 'bytes'
+        error_prefix: Literal['string', 'bytes'] = 'string' if issubclass(self.field_type, SecretStr) else 'bytes'
         if self.min_length is not None and len(value) < self.min_length:
             short_kind: core_schema.ErrorType = f'{error_prefix}_too_short'  # type: ignore[assignment]
             raise PydanticKnownError(short_kind, {'min_length': self.min_length})
@@ -771,8 +771,8 @@ def __get_pydantic_json_schema__(
     def __get_pydantic_core_schema__(
         self, source: type[Any], handler: _annotated_handlers.GetCoreSchemaHandler
     ) -> core_schema.CoreSchema:
-        self.inner_schema = handler(str if self.field_type is SecretStr else bytes)
-        error_kind = 'string_type' if self.field_type is SecretStr else 'bytes_type'
+        self.inner_schema = handler(str if issubclass(self.field_type, SecretStr) else bytes)
+        error_kind = 'string_type' if issubclass(self.field_type, SecretStr) else 'bytes_type'
         return core_schema.general_after_validator_function(
             self.validate,
             core_schema.union_schema(


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -3894,6 +3894,39 @@ class Foobar(BaseModel):
     assert f.empty_password.get_secret_value() == ''
 
 
+def test_secretstr_subclass():
+    class DecryptableStr(SecretStr):
+        """
+        Simulate a SecretStr with decryption capabilities.
+        """
+
+        def decrypt_value(self) -> str:
+            return f'MOCK DECRYPTED {self.get_secret_value()}'
+
+    class Foobar(BaseModel):
+        password: DecryptableStr
+        empty_password: SecretStr
+
+    # Initialize the model.
+    f = Foobar(password='1234', empty_password='')
+
+    # Assert correct types.
+    assert f.password.__class__.__name__ == 'DecryptableStr'
+    assert f.empty_password.__class__.__name__ == 'SecretStr'
+
+    # Assert str and repr are correct.
+    assert str(f.password) == '**********'
+    assert str(f.empty_password) == ''
+    assert repr(f.password) == "DecryptableStr('**********')"
+    assert repr(f.empty_password) == "SecretStr('')"
+    assert len(f.password) == 4
+    assert len(f.empty_password) == 0
+
+    # Assert retrieval of secret value is correct
+    assert f.password.get_secret_value() == '1234'
+    assert f.empty_password.get_secret_value() == ''
+
+
 def test_secretstr_equality():
     assert SecretStr('abc') == SecretStr('abc')
     assert SecretStr('123') != SecretStr('321')