Repository: pydantic/pydantic (Python)

Issue:
Base64Bytes field doesn't raises validation error for min_length constraint
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

The validation should raise an error due to the minimum length constraint set for the `data` field in the `Test` model. However, no validation error is raised even though the `data` field is empty.

### Expected Behavior:
Passing empty data for the `data` field should raise a validation error due to the minimum length constraint set in the `Test` model.

### Actual Behavior:
No validation error is raised when passing empty data for the `data` field.

### Steps to Reproduce:

Define the provided models in a Pydantic-based Python script.
Attempt to validate input data where the `data` field is empty.
Notice that no validation error is raised even though the `data` field does not meet the minimum length constraint.
Additional Information:
This behavior was observed in Pydantic version `2.6.4`.

### Example Code

```Python
from pydantic import Base64Bytes, BaseModel, Field


class Test(BaseModel):
    data: Base64Bytes = Field(min_length=10)


test = Test(data=b"")  # no validation error
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.6.4
pydantic-core version: 2.16.3
pydantic-core build: profile=release pgo=true
python version: 3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)]
platform: Windows-10-10.0.22631-SP0
```



Discussion:
The same behavior is true for versions:

```
$ pip freeze | grep pydantic
pydantic==2.7.0
pydantic_core==2.18.1
```


@kirillklimenko,

Thanks for reporting, definitely a bug! PRs welcome with a fix!
I would like to take a look at it! Where should I start looking at?
I wanna to contribute too, but, idk how to debug it, it's my first time trying to submit anything in this project. Any suggest?
I think this is related to encode, decode logic since even when converted `Base64Bytes` to `Base64Str`, there is no validation error too. We can avoid it by creating a custom validator, but I still don't know the exact cause. 🥲

I'd guess you could start with the core schema build - do we even attach the relevant metadata? Then, you might have to move into pydantic core to check the validation logic.



Patch:
diff --git a/pydantic/types.py b/pydantic/types.py
index 57816e03481..dc0069f5008 100644
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -2477,9 +2477,11 @@ def __get_pydantic_json_schema__(
         return field_schema
 
     def __get_pydantic_core_schema__(self, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
+        schema = handler(source)
+        _check_annotated_type(schema['type'], 'bytes', self.__class__.__name__)
         return core_schema.with_info_after_validator_function(
             function=self.decode,
-            schema=core_schema.bytes_schema(),
+            schema=schema,
             serialization=core_schema.plain_serializer_function_ser_schema(function=self.encode),
         )
 
@@ -2510,7 +2512,7 @@ def __hash__(self) -> int:
 
 
 @_dataclasses.dataclass(**_internal_dataclass.slots_true)
-class EncodedStr(EncodedBytes):
+class EncodedStr:
     """A str type that is encoded and decoded using the specified encoder.
 
     `EncodedStr` needs an encoder that implements `EncoderProtocol` to operate.
@@ -2564,14 +2566,25 @@ class Model(BaseModel):
     ```
     """
 
+    encoder: type[EncoderProtocol]
+
+    def __get_pydantic_json_schema__(
+        self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
+    ) -> JsonSchemaValue:
+        field_schema = handler(core_schema)
+        field_schema.update(type='string', format=self.encoder.get_json_format())
+        return field_schema
+
     def __get_pydantic_core_schema__(self, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
+        schema = handler(source)
+        _check_annotated_type(schema['type'], 'str', self.__class__.__name__)
         return core_schema.with_info_after_validator_function(
             function=self.decode_str,
-            schema=super(EncodedStr, self).__get_pydantic_core_schema__(source=source, handler=handler),  # noqa: UP008
+            schema=schema,
             serialization=core_schema.plain_serializer_function_ser_schema(function=self.encode_str),
         )
 
-    def decode_str(self, data: bytes, _: core_schema.ValidationInfo) -> str:
+    def decode_str(self, data: str, _: core_schema.ValidationInfo) -> str:
         """Decode the data using the specified encoder.
 
         Args:
@@ -2580,7 +2593,7 @@ def decode_str(self, data: bytes, _: core_schema.ValidationInfo) -> str:
         Returns:
             The decoded data.
         """
-        return data.decode()
+        return self.encoder.decode(data.encode()).decode()
 
     def encode_str(self, value: str) -> str:
         """Encode the data using the specified encoder.
@@ -2591,7 +2604,7 @@ def encode_str(self, value: str) -> str:
         Returns:
             The encoded data.
         """
-        return super(EncodedStr, self).encode(value=value.encode()).decode()  # noqa: UP008
+        return self.encoder.encode(value.encode()).decode()  # noqa: UP008
 
     def __hash__(self) -> int:
         return hash(self.encoder)


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
index 4d335f36320..69f5a46e9d9 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -7036,3 +7036,17 @@ class BeforeValidatorBeforeLe(BaseModel):
         BeforeValidatorBeforeLe(v=366)
     except ValueError as ex:
         assert 'datetime.timedelta(days=365)' in str(ex)
+
+
+@pytest.mark.parametrize('base64_type', [Base64Bytes, Base64Str, Base64UrlBytes, Base64UrlStr])
+def test_base64_with_invalid_min_length(base64_type) -> None:
+    """Check that an error is raised when the length of the base64 type's value is less or more than the min_length and max_length."""
+
+    class Model(BaseModel):
+        base64_value: base64_type = Field(min_length=3, max_length=5)  # type: ignore
+
+    with pytest.raises(ValidationError):
+        Model(**{'base64_value': b''})
+
+    with pytest.raises(ValidationError):
+        Model(**{'base64_value': b'123456'})