From 12e7e05613dc0075925dd5f160f01f7624aee5e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 08:59:00 +0100 Subject: [PATCH 1/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20JSON=20Sche?= =?UTF-8?q?ma=20for=20bytes,=20use=20`"contentMediaType":=20"application/o?= =?UTF-8?q?ctet-stream"`=20instead=20of=20`"format":=20"binary"`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pydantic/json_schema.py | 4 +++- tests/test_json_schema.py | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pydantic/json_schema.py b/pydantic/json_schema.py index 6662661012f..0d534155fb8 100644 --- a/pydantic/json_schema.py +++ b/pydantic/json_schema.py @@ -785,7 +785,9 @@ def bytes_schema(self, schema: core_schema.BytesSchema) -> JsonSchemaValue: Returns: The generated JSON schema. """ - json_schema = {'type': 'string', 'format': 'base64url' if self._config.ser_json_bytes == 'base64' else 'binary'} + json_schema = {'type': 'string', 'contentMediaType': 'application/octet-stream'} + if self._config.ser_json_bytes == 'base64': + json_schema['contentEncoding'] = 'base64' self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes) return json_schema diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py index ff6bc3c3fdf..e6bb98b7cdc 100644 --- a/tests/test_json_schema.py +++ b/tests/test_json_schema.py @@ -524,7 +524,7 @@ class Model(BaseModel): assert model_json_schema_validation == { 'properties': { - 'a': {'default': 'foobar', 'format': 'binary', 'title': 'A', 'type': 'string'}, + 'a': {'contentMediaType': 'application/octet-stream', 'default': 'foobar', 'title': 'A', 'type': 'string'}, 'b': { 'anyOf': [ {'type': 'number'}, @@ -542,7 +542,7 @@ class Model(BaseModel): } assert model_json_schema_serialization == { 'properties': { - 'a': {'default': 'foobar', 'format': 'binary', 'title': 'A', 'type': 'string'}, + 'a': {'contentMediaType': 'application/octet-stream', 'default': 'foobar', 'title': 'A', 'type': 'string'}, 'b': { 'default': '12.34', 'title': 'B', @@ -873,13 +873,13 @@ class Model(BaseModel): (Optional[str], {'properties': {'a': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'A'}}}), ( Optional[bytes], - {'properties': {'a': {'title': 'A', 'anyOf': [{'type': 'string', 'format': 'binary'}, {'type': 'null'}]}}}, + {'properties': {'a': {'title': 'A', 'anyOf': [{'contentMediaType': 'application/octet-stream', 'type': 'string'}, {'type': 'null'}]}}}, ), ( Union[str, bytes], { 'properties': { - 'a': {'title': 'A', 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}]} + 'a': {'title': 'A', 'anyOf': [{'type': 'string'}, {'contentMediaType': 'application/octet-stream', 'type': 'string'}]} }, }, ), @@ -889,7 +889,7 @@ class Model(BaseModel): 'properties': { 'a': { 'title': 'A', - 'anyOf': [{'type': 'string'}, {'type': 'string', 'format': 'binary'}, {'type': 'null'}], + 'anyOf': [{'type': 'string'}, {'contentMediaType': 'application/octet-stream', 'type': 'string'}, {'type': 'null'}], } } }, @@ -1876,8 +1876,8 @@ class Model(BaseModel): @pytest.mark.parametrize( 'ser_json_bytes,properties', [ - ('base64', {'data': {'default': 'Zm9vYmFy', 'format': 'base64url', 'title': 'Data', 'type': 'string'}}), - ('utf8', {'data': {'default': 'foobar', 'format': 'binary', 'title': 'Data', 'type': 'string'}}), + ('base64', {'data': {'default': 'Zm9vYmFy', 'contentEncoding': 'base64', 'contentMediaType': 'application/octet-stream', 'title': 'Data', 'type': 'string'}}), + ('utf8', {'data': {'contentMediaType': 'application/octet-stream', 'default': 'foobar', 'title': 'Data', 'type': 'string'}}), ], ) def test_model_default_bytes(ser_json_bytes: Literal['base64', 'utf8'], properties: dict[str, Any]): @@ -1917,8 +1917,8 @@ class Dataclass: @pytest.mark.parametrize( 'ser_json_bytes,properties', [ - ('base64', {'data': {'default': 'Zm9vYmFy', 'format': 'base64url', 'title': 'Data', 'type': 'string'}}), - ('utf8', {'data': {'default': 'foobar', 'format': 'binary', 'title': 'Data', 'type': 'string'}}), + ('base64', {'data': {'default': 'Zm9vYmFy', 'contentEncoding': 'base64', 'contentMediaType': 'application/octet-stream', 'title': 'Data', 'type': 'string'}}), + ('utf8', {'data': {'contentMediaType': 'application/octet-stream', 'default': 'foobar', 'title': 'Data', 'type': 'string'}}), ], ) def test_dataclass_default_bytes(ser_json_bytes: Literal['base64', 'utf8'], properties: dict[str, Any]): @@ -1958,8 +1958,8 @@ class MyTypedDict(TypedDict): @pytest.mark.parametrize( 'ser_json_bytes,properties', [ - ('base64', {'data': {'default': 'Zm9vYmFy', 'format': 'base64url', 'title': 'Data', 'type': 'string'}}), - ('utf8', {'data': {'default': 'foobar', 'format': 'binary', 'title': 'Data', 'type': 'string'}}), + ('base64', {'data': {'default': 'Zm9vYmFy', 'contentEncoding': 'base64', 'contentMediaType': 'application/octet-stream', 'title': 'Data', 'type': 'string'}}), + ('utf8', {'data': {'contentMediaType': 'application/octet-stream', 'default': 'foobar', 'title': 'Data', 'type': 'string'}}), ], ) def test_typeddict_default_bytes(ser_json_bytes: Literal['base64', 'utf8'], properties: dict[str, Any]): @@ -2018,7 +2018,7 @@ class A(BaseModel): ({'max_length': 5}, str, {'type': 'string', 'maxLength': 5}), ({}, constr(max_length=6), {'type': 'string', 'maxLength': 6}), ({'min_length': 2}, str, {'type': 'string', 'minLength': 2}), - ({'max_length': 5}, bytes, {'type': 'string', 'maxLength': 5, 'format': 'binary'}), + ({'max_length': 5}, bytes, {'contentMediaType': 'application/octet-stream', 'type': 'string', 'maxLength': 5}), ({'pattern': '^foo$'}, str, {'type': 'string', 'pattern': '^foo$'}), ({'gt': 2}, int, {'type': 'integer', 'exclusiveMinimum': 2}), ({'lt': 5}, int, {'type': 'integer', 'exclusiveMaximum': 5}), @@ -2122,7 +2122,7 @@ class Foo(BaseModel): ({'max_length': 5}, str, {'type': 'string', 'maxLength': 5}), ({}, constr(max_length=6), {'type': 'string', 'maxLength': 6}), ({'min_length': 2}, str, {'type': 'string', 'minLength': 2}), - ({'max_length': 5}, bytes, {'type': 'string', 'maxLength': 5, 'format': 'binary'}), + ({'max_length': 5}, bytes, {'contentMediaType': 'application/octet-stream', 'type': 'string', 'maxLength': 5}), ({'pattern': '^foo$'}, str, {'type': 'string', 'pattern': '^foo$'}), ({'gt': 2}, int, {'type': 'integer', 'exclusiveMinimum': 2}), ({'lt': 5}, int, {'type': 'integer', 'exclusiveMaximum': 5}), @@ -2295,7 +2295,7 @@ class Foo(BaseModel): # (ConstrainedBytes, {'title': 'A', 'type': 'string', 'format': 'binary'}), ( conbytes(min_length=3, max_length=5), - {'title': 'A', 'type': 'string', 'format': 'binary', 'minLength': 3, 'maxLength': 5}, + {'title': 'A', 'contentMediaType': 'application/octet-stream', 'type': 'string', 'minLength': 3, 'maxLength': 5}, ), ], ) From 417f6e2853400f0ff5eb9ff88ab509f2afda48ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 12:43:51 +0100 Subject: [PATCH 2/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Handle=20base64=20JSON?= =?UTF-8?q?=20Schema=20for=20validation=20and=20serialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pydantic/json_schema.py | 3 +- tests/test_json_schema.py | 116 +++++++++++++++++++++++++++++++++----- tests/test_types.py | 7 ++- 3 files changed, 111 insertions(+), 15 deletions(-) diff --git a/pydantic/json_schema.py b/pydantic/json_schema.py index 0d534155fb8..a0835bad108 100644 --- a/pydantic/json_schema.py +++ b/pydantic/json_schema.py @@ -786,7 +786,8 @@ def bytes_schema(self, schema: core_schema.BytesSchema) -> JsonSchemaValue: The generated JSON schema. """ json_schema = {'type': 'string', 'contentMediaType': 'application/octet-stream'} - if self._config.ser_json_bytes == 'base64': + bytes_mode = self._config.ser_json_bytes if self.mode == 'serialization' else self._config.val_json_bytes + if bytes_mode == 'base64': json_schema['contentEncoding'] = 'base64' self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes) return json_schema diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py index e6bb98b7cdc..b4f1a26eee1 100644 --- a/tests/test_json_schema.py +++ b/tests/test_json_schema.py @@ -873,13 +873,26 @@ class Model(BaseModel): (Optional[str], {'properties': {'a': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'A'}}}), ( Optional[bytes], - {'properties': {'a': {'title': 'A', 'anyOf': [{'contentMediaType': 'application/octet-stream', 'type': 'string'}, {'type': 'null'}]}}}, + { + 'properties': { + 'a': { + 'title': 'A', + 'anyOf': [{'contentMediaType': 'application/octet-stream', 'type': 'string'}, {'type': 'null'}], + } + } + }, ), ( Union[str, bytes], { 'properties': { - 'a': {'title': 'A', 'anyOf': [{'type': 'string'}, {'contentMediaType': 'application/octet-stream', 'type': 'string'}]} + 'a': { + 'title': 'A', + 'anyOf': [ + {'type': 'string'}, + {'contentMediaType': 'application/octet-stream', 'type': 'string'}, + ], + } }, }, ), @@ -889,7 +902,11 @@ class Model(BaseModel): 'properties': { 'a': { 'title': 'A', - 'anyOf': [{'type': 'string'}, {'contentMediaType': 'application/octet-stream', 'type': 'string'}, {'type': 'null'}], + 'anyOf': [ + {'type': 'string'}, + {'contentMediaType': 'application/octet-stream', 'type': 'string'}, + {'type': 'null'}, + ], } } }, @@ -994,6 +1011,8 @@ class Model(BaseModel): 'properties': {'a': {'title': 'A', 'type': inner_type, 'writeOnly': True, 'format': 'password'}}, 'required': ['a'], } + if field_type is SecretBytes: + base_schema['properties']['a']['contentMediaType'] = 'application/octet-stream' assert Model.model_json_schema() == base_schema @@ -1876,8 +1895,29 @@ class Model(BaseModel): @pytest.mark.parametrize( 'ser_json_bytes,properties', [ - ('base64', {'data': {'default': 'Zm9vYmFy', 'contentEncoding': 'base64', 'contentMediaType': 'application/octet-stream', 'title': 'Data', 'type': 'string'}}), - ('utf8', {'data': {'contentMediaType': 'application/octet-stream', 'default': 'foobar', 'title': 'Data', 'type': 'string'}}), + ( + 'base64', + { + 'data': { + 'default': 'Zm9vYmFy', + 'contentEncoding': 'base64', + 'contentMediaType': 'application/octet-stream', + 'title': 'Data', + 'type': 'string', + } + }, + ), + ( + 'utf8', + { + 'data': { + 'contentMediaType': 'application/octet-stream', + 'default': 'foobar', + 'title': 'Data', + 'type': 'string', + } + }, + ), ], ) def test_model_default_bytes(ser_json_bytes: Literal['base64', 'utf8'], properties: dict[str, Any]): @@ -1917,8 +1957,29 @@ class Dataclass: @pytest.mark.parametrize( 'ser_json_bytes,properties', [ - ('base64', {'data': {'default': 'Zm9vYmFy', 'contentEncoding': 'base64', 'contentMediaType': 'application/octet-stream', 'title': 'Data', 'type': 'string'}}), - ('utf8', {'data': {'contentMediaType': 'application/octet-stream', 'default': 'foobar', 'title': 'Data', 'type': 'string'}}), + ( + 'base64', + { + 'data': { + 'default': 'Zm9vYmFy', + 'contentEncoding': 'base64', + 'contentMediaType': 'application/octet-stream', + 'title': 'Data', + 'type': 'string', + } + }, + ), + ( + 'utf8', + { + 'data': { + 'contentMediaType': 'application/octet-stream', + 'default': 'foobar', + 'title': 'Data', + 'type': 'string', + } + }, + ), ], ) def test_dataclass_default_bytes(ser_json_bytes: Literal['base64', 'utf8'], properties: dict[str, Any]): @@ -1958,8 +2019,29 @@ class MyTypedDict(TypedDict): @pytest.mark.parametrize( 'ser_json_bytes,properties', [ - ('base64', {'data': {'default': 'Zm9vYmFy', 'contentEncoding': 'base64', 'contentMediaType': 'application/octet-stream', 'title': 'Data', 'type': 'string'}}), - ('utf8', {'data': {'contentMediaType': 'application/octet-stream', 'default': 'foobar', 'title': 'Data', 'type': 'string'}}), + ( + 'base64', + { + 'data': { + 'default': 'Zm9vYmFy', + 'contentEncoding': 'base64', + 'contentMediaType': 'application/octet-stream', + 'title': 'Data', + 'type': 'string', + } + }, + ), + ( + 'utf8', + { + 'data': { + 'contentMediaType': 'application/octet-stream', + 'default': 'foobar', + 'title': 'Data', + 'type': 'string', + } + }, + ), ], ) def test_typeddict_default_bytes(ser_json_bytes: Literal['base64', 'utf8'], properties: dict[str, Any]): @@ -2295,7 +2377,13 @@ class Foo(BaseModel): # (ConstrainedBytes, {'title': 'A', 'type': 'string', 'format': 'binary'}), ( conbytes(min_length=3, max_length=5), - {'title': 'A', 'contentMediaType': 'application/octet-stream', 'type': 'string', 'minLength': 3, 'maxLength': 5}, + { + 'title': 'A', + 'contentMediaType': 'application/octet-stream', + 'type': 'string', + 'minLength': 3, + 'maxLength': 5, + }, ), ], ) @@ -4642,12 +4730,14 @@ def test_secrets_schema(secret_cls, field_kw, schema_kw): class Foobar(BaseModel): password: secret_cls = Field(**field_kw) + expected_props = {'title': 'Password', 'type': 'string', 'writeOnly': True, 'format': 'password', **schema_kw} + if secret_cls is SecretBytes: + expected_props['contentMediaType'] = 'application/octet-stream' + assert Foobar.model_json_schema() == { 'title': 'Foobar', 'type': 'object', - 'properties': { - 'password': {'title': 'Password', 'type': 'string', 'writeOnly': True, 'format': 'password', **schema_kw} - }, + 'properties': {'password': expected_props}, 'required': ['password'], } diff --git a/tests/test_types.py b/tests/test_types.py index 9cb3fe29b78..96df625b9b0 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -3547,7 +3547,12 @@ class Model(BaseModel): assert Model.model_json_schema() == { 'properties': { 'str_type': {'format': 'path', 'title': 'Str Type', 'type': 'string'}, - 'byte_type': {'format': 'path', 'title': 'Byte Type', 'type': 'string'}, + 'byte_type': { + 'contentMediaType': 'application/octet-stream', + 'format': 'path', + 'title': 'Byte Type', + 'type': 'string', + }, 'any_type': {'format': 'path', 'title': 'Any Type', 'type': 'string'}, }, 'required': ['str_type', 'byte_type', 'any_type'], From 401c2f667b492c643764d1088464d58f2684a97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 12:52:59 +0100 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20contentEncoding=20of?= =?UTF-8?q?=20base64=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pydantic/types.py | 4 ++-- tests/test_types.py | 24 ++++++++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/pydantic/types.py b/pydantic/types.py index 32742ac4645..68aa01ee75c 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -2529,7 +2529,7 @@ 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()) + field_schema.update(type='string', contentEncoding=self.encoder.get_json_format()) return field_schema def __get_pydantic_core_schema__(self, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: @@ -2628,7 +2628,7 @@ 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()) + field_schema.update(type='string', contentEncoding=self.encoder.get_json_format()) return field_schema def __get_pydantic_core_schema__(self, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: diff --git a/tests/test_types.py b/tests/test_types.py index 96df625b9b0..ac67ff8bc16 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -5604,15 +5604,19 @@ class Model(BaseModel): 'base64_value_or_none': None, } + if field_type in (Base64Bytes,): + base64_schema = {'contentEncoding': 'base64', 'contentMediaType': 'application/octet-stream', 'type': 'string'} + else: + base64_schema = {'contentEncoding': 'base64', 'type': 'string'} + assert Model.model_json_schema() == { 'properties': { 'base64_value': { - 'format': 'base64', + **base64_schema, 'title': 'Base64 Value', - 'type': 'string', }, 'base64_value_or_none': { - 'anyOf': [{'type': 'string', 'format': 'base64'}, {'type': 'null'}], + 'anyOf': [base64_schema, {'type': 'null'}], 'default': None, 'title': 'Base64 Value Or None', }, @@ -5697,15 +5701,23 @@ class Model(BaseModel): 'base64url_value_or_none': None, } + if field_type in (Base64UrlBytes,): + base64url_schema = { + 'contentEncoding': 'base64url', + 'contentMediaType': 'application/octet-stream', + 'type': 'string', + } + else: + base64url_schema = {'contentEncoding': 'base64url', 'type': 'string'} + assert Model.model_json_schema() == { 'properties': { 'base64url_value': { - 'format': 'base64url', + **base64url_schema, 'title': 'Base64Url Value', - 'type': 'string', }, 'base64url_value_or_none': { - 'anyOf': [{'type': 'string', 'format': 'base64url'}, {'type': 'null'}], + 'anyOf': [base64url_schema, {'type': 'null'}], 'default': None, 'title': 'Base64Url Value Or None', }, From d24c8abd429474bc8883c408cfc8628195c90c23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 13:00:45 +0100 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20with=20bytes?= =?UTF-8?q?=20JSON=20Schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/why.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/why.md b/docs/why.md index cbdfdc9fcb0..7e2da84268e 100644 --- a/docs/why.md +++ b/docs/why.md @@ -314,7 +314,7 @@ Pydantic provides four ways to create schemas and perform validation and seriali { 'properties': { 'when': {'format': 'date-time', 'title': 'When', 'type': 'string'}, - 'where': {'format': 'binary', 'title': 'Where', 'type': 'string'}, + 'where': {'contentMediaType': 'application/octet-stream', 'title': 'Where', 'type': 'string'}, 'why': {'title': 'Why', 'type': 'string'}, }, 'required': ['when', 'where'], From 773f94983946ece968880e14b2b1821d227abfa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 21 Feb 2026 13:05:08 +0100 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/why.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/why.md b/docs/why.md index 7e2da84268e..002c342e465 100644 --- a/docs/why.md +++ b/docs/why.md @@ -314,7 +314,11 @@ Pydantic provides four ways to create schemas and perform validation and seriali { 'properties': { 'when': {'format': 'date-time', 'title': 'When', 'type': 'string'}, - 'where': {'contentMediaType': 'application/octet-stream', 'title': 'Where', 'type': 'string'}, + 'where': { + 'contentMediaType': 'application/octet-stream', + 'title': 'Where', + 'type': 'string', + }, 'why': {'title': 'Why', 'type': 'string'}, }, 'required': ['when', 'where'], From 69710dd1cce527e2e32f611f70f1797dfb1e8c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 3 Apr 2026 14:53:36 +0200 Subject: [PATCH 6/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20with=20code?= =?UTF-8?q?=20review,=20keep=20format,=20add=20contentEncoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pydantic/types.py | 49 ++++++++++++++++++++++++++++++++++++++++----- tests/test_types.py | 12 ++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/pydantic/types.py b/pydantic/types.py index 68aa01ee75c..80b6745caba 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -2379,11 +2379,20 @@ def encode(cls, value: bytes) -> bytes: ... @classmethod - def get_json_format(cls) -> str: - """Get the JSON format for the encoded data. + def get_json_format(cls) -> str | None: + """Get the JSON Schema `format` value for the encoded data. Returns: - The JSON format for the encoded data. + The format string, or `None` if no format should be set. + """ + ... + + @classmethod + def get_content_encoding(cls) -> str | None: + """Get the JSON Schema `contentEncoding` value for the encoded data. + + Returns: + The content encoding string, or `None` if no content encoding should be set. """ ... @@ -2427,6 +2436,15 @@ def get_json_format(cls) -> Literal['base64']: """ return 'base64' + @classmethod + def get_content_encoding(cls) -> Literal['base64']: + """Get the JSON Schema `contentEncoding` value for the encoded data. + + Returns: + The content encoding string. + """ + return 'base64' + class Base64UrlEncoder(EncoderProtocol): """URL-safe Base64 encoder.""" @@ -2467,6 +2485,15 @@ def get_json_format(cls) -> Literal['base64url']: """ return 'base64url' + @classmethod + def get_content_encoding(cls) -> Literal['base64url']: + """Get the JSON Schema `contentEncoding` value for the encoded data. + + Returns: + The content encoding string. + """ + return 'base64url' + @_dataclasses.dataclass(**_internal_dataclass.slots_true) class EncodedBytes: @@ -2529,7 +2556,13 @@ def __get_pydantic_json_schema__( self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler ) -> JsonSchemaValue: field_schema = handler(core_schema) - field_schema.update(type='string', contentEncoding=self.encoder.get_json_format()) + field_schema.update(type='string') + json_format = self.encoder.get_json_format() + if json_format is not None: + field_schema['format'] = json_format + content_encoding = self.encoder.get_content_encoding() + if content_encoding is not None: + field_schema['contentEncoding'] = content_encoding return field_schema def __get_pydantic_core_schema__(self, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: @@ -2628,7 +2661,13 @@ def __get_pydantic_json_schema__( self, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler ) -> JsonSchemaValue: field_schema = handler(core_schema) - field_schema.update(type='string', contentEncoding=self.encoder.get_json_format()) + field_schema.update(type='string') + json_format = self.encoder.get_json_format() + if json_format is not None: + field_schema['format'] = json_format + content_encoding = self.encoder.get_content_encoding() + if content_encoding is not None: + field_schema['contentEncoding'] = content_encoding return field_schema def __get_pydantic_core_schema__(self, source: type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: diff --git a/tests/test_types.py b/tests/test_types.py index ac67ff8bc16..4b45d5f31b0 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -5605,9 +5605,14 @@ class Model(BaseModel): } if field_type in (Base64Bytes,): - base64_schema = {'contentEncoding': 'base64', 'contentMediaType': 'application/octet-stream', 'type': 'string'} + base64_schema = { + 'contentEncoding': 'base64', + 'contentMediaType': 'application/octet-stream', + 'format': 'base64', + 'type': 'string', + } else: - base64_schema = {'contentEncoding': 'base64', 'type': 'string'} + base64_schema = {'contentEncoding': 'base64', 'format': 'base64', 'type': 'string'} assert Model.model_json_schema() == { 'properties': { @@ -5705,10 +5710,11 @@ class Model(BaseModel): base64url_schema = { 'contentEncoding': 'base64url', 'contentMediaType': 'application/octet-stream', + 'format': 'base64url', 'type': 'string', } else: - base64url_schema = {'contentEncoding': 'base64url', 'type': 'string'} + base64url_schema = {'contentEncoding': 'base64url', 'format': 'base64url', 'type': 'string'} assert Model.model_json_schema() == { 'properties': {