diff --git a/sdk/ml/azure-ai-ml/CHANGELOG.md b/sdk/ml/azure-ai-ml/CHANGELOG.md index bc3df33c6c49..ccf329d785ff 100644 --- a/sdk/ml/azure-ai-ml/CHANGELOG.md +++ b/sdk/ml/azure-ai-ml/CHANGELOG.md @@ -11,7 +11,7 @@ ## 1.34.1 (unreleased) ### Bugs Fixed -- Fixed `BatchEndpoint` defaults serialization regression where `deployment_name` was sent to the service as snake_case instead of camelCase (`deploymentName`), causing `begin_create_or_update` to fail with "Could not find member 'deployment_name' on object of type 'BatchEndpointDefaults'". `BatchEndpoint.defaults` is now consistently exposed as a snake_case dict to users and converted to the correct wire format on serialization. +- Fixed `BatchEndpoint` defaults serialization regression where `deployment_name` was sent to the service as snake_case instead of camelCase (`deploymentName`), causing `begin_create_or_update` to fail with "Could not find member 'deployment_name' on object of type 'BatchEndpointDefaults'". Serialization now emits the correct camelCase wire format, while `BatchEndpoint.defaults` returned from `get()` continues to expose an object that supports attribute access (e.g. `endpoint.defaults.deployment_name`), preserving backward compatibility with existing code and samples. ## 1.34.0 (2026-06-11) diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/_schema/_endpoint/batch/batch_endpoint_defaults.py b/sdk/ml/azure-ai-ml/azure/ai/ml/_schema/_endpoint/batch/batch_endpoint_defaults.py index ef6cd6ff24b5..c4d7e8372773 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/_schema/_endpoint/batch/batch_endpoint_defaults.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/_schema/_endpoint/batch/batch_endpoint_defaults.py @@ -9,6 +9,7 @@ from marshmallow import fields, post_load +from azure.ai.ml._restclient.arm_ml_service.models import BatchEndpointDefaults from azure.ai.ml._schema.core.schema import PatchedSchemaMeta module_logger = logging.getLogger(__name__) @@ -24,4 +25,4 @@ class BatchEndpointsDefaultsSchema(metaclass=PatchedSchemaMeta): @post_load def make(self, data: Any, **kwargs: Any) -> Any: - return dict(data) + return BatchEndpointDefaults(**data) diff --git a/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_endpoint/batch_endpoint.py b/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_endpoint/batch_endpoint.py index c638a5d6bb54..466e19131344 100644 --- a/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_endpoint/batch_endpoint.py +++ b/sdk/ml/azure-ai-ml/azure/ai/ml/entities/_endpoint/batch_endpoint.py @@ -97,14 +97,12 @@ def _to_rest_batch_endpoint(self, location: str) -> BatchEndpointData: @classmethod def _from_rest_object(cls, obj: BatchEndpointData) -> "BatchEndpoint": - defaults: Optional[Dict[str, str]] = None - rest_defaults = obj.properties.defaults - if isinstance(rest_defaults, RestBatchEndpointDefaults): - if rest_defaults.deployment_name is not None: - defaults = {"deployment_name": rest_defaults.deployment_name} - elif isinstance(rest_defaults, dict): - defaults = dict(rest_defaults) if rest_defaults else None - + # Pass the REST ``BatchEndpointDefaults`` object through unchanged so that + # ``endpoint.defaults.deployment_name`` remains gettable/settable. Callers + # (including the published batch-endpoint sample notebooks) rely on being + # able to do ``endpoint.defaults.deployment_name = `` after a get(). + # Serialization back to camelCase wire format is handled in + # ``_to_rest_batch_endpoint`` via the ``RestBatchEndpointDefaults`` branch. return BatchEndpoint( id=obj.id, name=obj.name, @@ -113,7 +111,7 @@ def _from_rest_object(cls, obj: BatchEndpointData) -> "BatchEndpoint": auth_mode=camel_to_snake(obj.properties.auth_mode), description=obj.properties.description, location=obj.location, - defaults=defaults, + defaults=obj.properties.defaults, provisioning_state=obj.properties.provisioning_state, scoring_uri=obj.properties.scoring_uri, openapi_uri=obj.properties.swagger_uri, diff --git a/sdk/ml/azure-ai-ml/tests/batch_online_common/unittests/test_endpoint_entity.py b/sdk/ml/azure-ai-ml/tests/batch_online_common/unittests/test_endpoint_entity.py index 4b7b0d530385..e52adbf28037 100644 --- a/sdk/ml/azure-ai-ml/tests/batch_online_common/unittests/test_endpoint_entity.py +++ b/sdk/ml/azure-ai-ml/tests/batch_online_common/unittests/test_endpoint_entity.py @@ -205,14 +205,31 @@ def test_to_rest_batch_endpoint_with_no_defaults_passes_none(self) -> None: assert rest_batch_endpoint.properties.defaults is None - def test_from_rest_object_defaults_returned_as_snake_case_dict(self) -> None: + def test_from_rest_object_defaults_supports_attribute_access(self) -> None: with open(TestBatchEndpointYAML.BATCH_ENDPOINT_REST, "r") as f: batch_endpoint_rest = _deserialize(BatchEndpointData, json.load(f)) batch_endpoint = BatchEndpoint._from_rest_object(batch_endpoint_rest) - assert batch_endpoint.defaults == {"deployment_name": "hello-world-1"} - assert batch_endpoint.defaults["deployment_name"] == "hello-world-1" + # ``endpoint.defaults`` must remain an object that supports attribute + # get/set so the published sample pattern keeps working: + # endpoint.defaults.deployment_name = deployment.name + assert batch_endpoint.defaults.deployment_name == "hello-world-1" + + def test_from_rest_object_defaults_roundtrips_to_camel_case(self) -> None: + with open(TestBatchEndpointYAML.BATCH_ENDPOINT_REST, "r") as f: + batch_endpoint_rest = _deserialize(BatchEndpointData, json.load(f)) + + batch_endpoint = BatchEndpoint._from_rest_object(batch_endpoint_rest) + + # Emulate the sample notebook flow: mutate the default deployment on the + # object returned by get() and serialize it back to the wire format. + batch_endpoint.defaults.deployment_name = "hello-world-2" + rest_batch_endpoint = batch_endpoint._to_rest_batch_endpoint("eastus") + + rest_defaults = rest_batch_endpoint.properties.defaults + assert rest_defaults.deployment_name == "hello-world-2" + assert rest_defaults.as_dict() == {"deploymentName": "hello-world-2"} class TestKubernetesOnlineEndopint: