diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc3b2d2d..b438aafe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/kernel-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8935e932..b8dda9bf 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.28.0" + ".": "0.29.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index bcbec15d..470e4983 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 97 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-d430a8e3407ceb608d912cabadbcb016b4fcf057ca56b3bbd179ea3b3121b484.yml -openapi_spec_hash: 8adbf013baf77abacaf04ed067749397 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-46c8320dcd9f8fc596f469ef0dd1aafaca591ab36cf2a6f8a7297dc9136bdc71.yml +openapi_spec_hash: 1be1e6589cd94c581b241720e01a65bc config_hash: b470456b217bb9502f5212311d395a6f diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b162d16..1f736cba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.29.0 (2026-01-30) + +Full Changelog: [v0.28.0...v0.29.0](https://github.com/kernel/kernel-python-sdk/compare/v0.28.0...v0.29.0) + +### Features + +* add support for 1280x800@60 viewport ([1cb6575](https://github.com/kernel/kernel-python-sdk/commit/1cb65752f6aa45fcb2ca1dd55f0eebf9457abfa9)) +* **client:** add custom JSON encoder for extended type support ([33604fe](https://github.com/kernel/kernel-python-sdk/commit/33604feb1a07bbca94477c28143052d5c6eff70d)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([4828aba](https://github.com/kernel/kernel-python-sdk/commit/4828aba4349e61686285b57358892825e75286be)) + ## 0.28.0 (2026-01-22) Full Changelog: [v0.27.0...v0.28.0](https://github.com/kernel/kernel-python-sdk/compare/v0.27.0...v0.28.0) diff --git a/pyproject.toml b/pyproject.toml index 4402e482..66433700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.28.0" +version = "0.29.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index 07809c5b..a3d47eaf 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/kernel/_compat.py b/src/kernel/_compat.py index bdef67f0..786ff42a 100644 --- a/src/kernel/_compat.py +++ b/src/kernel/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/kernel/_utils/_json.py b/src/kernel/_utils/_json.py new file mode 100644 index 00000000..60584214 --- /dev/null +++ b/src/kernel/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/src/kernel/_version.py b/src/kernel/_version.py index f78bed97..d24a1a6c 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.28.0" # x-release-please-version +__version__ = "0.29.0" # x-release-please-version diff --git a/src/kernel/resources/browser_pools.py b/src/kernel/resources/browser_pools.py index 884b0e16..4f6ad8b2 100644 --- a/src/kernel/resources/browser_pools.py +++ b/src/kernel/resources/browser_pools.py @@ -111,8 +111,8 @@ def create( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -243,8 +243,8 @@ def update( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -549,8 +549,8 @@ async def create( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -681,8 +681,8 @@ async def update( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py index 534ae062..4b61d8d8 100644 --- a/src/kernel/resources/browsers/browsers.py +++ b/src/kernel/resources/browsers/browsers.py @@ -188,8 +188,8 @@ def create( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser @@ -599,8 +599,8 @@ async def create( image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, - 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be - automatically determined from the width and height if they match a supported + 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will + be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 0818760c..1e93fa6d 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -72,8 +72,8 @@ class BrowserCreateParams(TypedDict, total=False): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index efff854f..b6c28acf 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -57,8 +57,8 @@ class BrowserCreateResponse(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 3ce26488..d99546d5 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -57,8 +57,8 @@ class BrowserListResponse(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_pool.py b/src/kernel/types/browser_pool.py index b9142f7c..fbbf2cb6 100644 --- a/src/kernel/types/browser_pool.py +++ b/src/kernel/types/browser_pool.py @@ -73,10 +73,10 @@ class BrowserPoolConfig(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 4b70a87f..3175b398 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -57,8 +57,8 @@ class BrowserPoolAcquireResponse(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_pool_create_params.py b/src/kernel/types/browser_pool_create_params.py index afa9e6e5..81deaa68 100644 --- a/src/kernel/types/browser_pool_create_params.py +++ b/src/kernel/types/browser_pool_create_params.py @@ -72,8 +72,8 @@ class BrowserPoolCreateParams(TypedDict, total=False): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_pool_update_params.py b/src/kernel/types/browser_pool_update_params.py index ac23916a..63487086 100644 --- a/src/kernel/types/browser_pool_update_params.py +++ b/src/kernel/types/browser_pool_update_params.py @@ -78,8 +78,8 @@ class BrowserPoolUpdateParams(TypedDict, total=False): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 12f58a5e..09210e8c 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -57,8 +57,8 @@ class BrowserRetrieveResponse(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index d5cb3150..01c34be5 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -57,8 +57,8 @@ class BrowserUpdateResponse(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, - 1440x900@25, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will - be automatically determined from the width and height if they match a supported - configuration exactly. Note: Higher resolutions may affect the responsiveness of - live view browser + 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not + provided, it will be automatically determined from the width and height if they + match a supported configuration exactly. Note: Higher resolutions may affect the + responsiveness of live view browser """ diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py index ab8f4273..2329dd75 100644 --- a/src/kernel/types/shared/browser_viewport.py +++ b/src/kernel/types/shared/browser_viewport.py @@ -12,7 +12,7 @@ class BrowserViewport(BaseModel): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser """ diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py index 9236547a..7041ea55 100644 --- a/src/kernel/types/shared_params/browser_viewport.py +++ b/src/kernel/types/shared_params/browser_viewport.py @@ -12,7 +12,7 @@ class BrowserViewport(TypedDict, total=False): If omitted, image defaults apply (1920x1080@25). Only specific viewport configurations are supported. The server will reject unsupported combinations. - Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60 + Supported resolutions are: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1280x800@60, 1024x768@60, 1200x800@60 If refresh_rate is not provided, it will be automatically determined from the width and height if they match a supported configuration exactly. Note: Higher resolutions may affect the responsiveness of live view browser """ diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 00000000..20e385b9 --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from kernel import _compat +from kernel._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}'