From 75618111fcd836f23d6540a5007c13a5b43c0896 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Thu, 15 Oct 2026 18:03:13 +0200 Subject: [PATCH] fix(json): give on_start the object's own writer and pass the hooks to nested parse nodes JsonSerializationWriter called on_start_object_serialization with the outer writer, but the object is written into a temp writer. The backing store uses on_start to write null for properties set to None. So clearing a property on the root model raised "Invalid Json output", and on a nested model the null went into the parent object. on_start now gets the temp writer, which is the writer the object is written to, like in C#. JsonParseNode built each field's node without the before and after hooks, so they only ran for the root object. Field nodes now come from _create_new_node, which copies them, like the C# parse node. --- .../json_parse_node.py | 2 +- .../json_serialization_writer.py | 2 +- .../json/tests/helpers/__init__.py | 1 + .../serialization/json/tests/helpers/team.py | 24 +++++++++++++++++++ .../json/tests/unit/test_json_parse_node.py | 12 +++++++++- .../unit/test_json_serialization_writer.py | 24 ++++++++++++++++++- 6 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 packages/serialization/json/tests/helpers/team.py diff --git a/packages/serialization/json/kiota_serialization_json/json_parse_node.py b/packages/serialization/json/kiota_serialization_json/json_parse_node.py index dda72087..45b023b5 100644 --- a/packages/serialization/json/kiota_serialization_json/json_parse_node.py +++ b/packages/serialization/json/kiota_serialization_json/json_parse_node.py @@ -351,7 +351,7 @@ def _assign_field_values(self, item: Parsable) -> None: if field_value is None: continue field_deserializer = field_deserializers[field_name] - field_deserializer(JsonParseNode(field_value)) + field_deserializer(self._create_new_node(field_value)) elif item_additional_data is not None: item_additional_data[field_name] = self.try_get_anything(field_value) else: diff --git a/packages/serialization/json/kiota_serialization_json/json_serialization_writer.py b/packages/serialization/json/kiota_serialization_json/json_serialization_writer.py index 695b6e98..036f458d 100644 --- a/packages/serialization/json/kiota_serialization_json/json_serialization_writer.py +++ b/packages/serialization/json/kiota_serialization_json/json_serialization_writer.py @@ -519,7 +519,7 @@ def _serialize_value(self, temp_writer: JsonSerializationWriter, value: U) -> No if on_before := self.on_before_object_serialization: on_before(value) if on_start := self.on_start_object_serialization: - on_start(value, self) + on_start(value, temp_writer) value.serialize(temp_writer) diff --git a/packages/serialization/json/tests/helpers/__init__.py b/packages/serialization/json/tests/helpers/__init__.py index f2c0b4a9..8e759d04 100644 --- a/packages/serialization/json/tests/helpers/__init__.py +++ b/packages/serialization/json/tests/helpers/__init__.py @@ -1,4 +1,5 @@ from .entity import Entity from .office_location import OfficeLocation +from .team import Team from .user import User from .user2 import User2 diff --git a/packages/serialization/json/tests/helpers/team.py b/packages/serialization/json/tests/helpers/team.py new file mode 100644 index 00000000..49c354d0 --- /dev/null +++ b/packages/serialization/json/tests/helpers/team.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from dataclasses import dataclass +from collections.abc import Callable +from typing import Optional + +from kiota_abstractions.serialization import Parsable, ParseNode, SerializationWriter + +from .user import User + + +@dataclass +class Team(Parsable): + lead: Optional[User] = None + + @staticmethod + def create_from_discriminator_value(parse_node: Optional[ParseNode] = None) -> Team: + return Team() + + def get_field_deserializers(self) -> dict[str, Callable[[ParseNode], None]]: + return {"lead": lambda n: setattr(self, 'lead', n.get_object_value(User))} + + def serialize(self, writer: SerializationWriter) -> None: + writer.write_object_value("lead", self.lead) diff --git a/packages/serialization/json/tests/unit/test_json_parse_node.py b/packages/serialization/json/tests/unit/test_json_parse_node.py index c5d59dc8..489874df 100644 --- a/packages/serialization/json/tests/unit/test_json_parse_node.py +++ b/packages/serialization/json/tests/unit/test_json_parse_node.py @@ -4,7 +4,7 @@ import pytest from kiota_serialization_json.json_parse_node import JsonParseNode -from ..helpers import OfficeLocation, User +from ..helpers import OfficeLocation, Team, User def test_get_str_value(): @@ -363,3 +363,13 @@ def test_get_collection_of_object_values(users_json): assert result[0].business_phones == ["+1 205 555 0108"] assert result[0].is_active is True assert result[0].mobile_phone is None + + +def test_hooks_run_for_nested_objects(): + seen = [] + parse_node = JsonParseNode(json.loads('{"lead": {"mobile_phone": "+1 234 567 891"}}')) + parse_node.on_before_assign_field_values = lambda value: seen.append(type(value).__name__) + parse_node.on_after_assign_field_values = lambda value: seen.append(type(value).__name__) + team = parse_node.get_object_value(Team) + assert team.lead.mobile_phone == "+1 234 567 891" + assert seen == ["Team", "User", "User", "Team"] diff --git a/packages/serialization/json/tests/unit/test_json_serialization_writer.py b/packages/serialization/json/tests/unit/test_json_serialization_writer.py index d115c1bc..69ef56e6 100644 --- a/packages/serialization/json/tests/unit/test_json_serialization_writer.py +++ b/packages/serialization/json/tests/unit/test_json_serialization_writer.py @@ -6,7 +6,7 @@ from kiota_serialization_json.json_serialization_writer import JsonSerializationWriter -from ..helpers import OfficeLocation, User, User2 +from ..helpers import OfficeLocation, Team, User, User2 @pytest.fixture @@ -353,3 +353,25 @@ def test_write_composed_type_with_no_key(user_1): # The union type should serialize the inner user object, not an empty object assert content_string == '{"id": "8f841f30-e6e3-439a-a812-ebd369559c36", '\ '"updated_at": "2022-01-27T12:59:45.596117+00:00", "is_active": true}' + + +def clear_is_active(value, writer): + if isinstance(value, User): + writer.write_null_value("is_active") + + +def test_on_start_writes_into_the_root_object(): + json_serialization_writer = JsonSerializationWriter() + json_serialization_writer.on_start_object_serialization = clear_is_active + json_serialization_writer.write_object_value(None, User(mobile_phone="+1 234 567 891")) + content = json_serialization_writer.get_serialized_content() + assert json.loads(content) == {"is_active": None, "mobile_phone": "+1 234 567 891"} + + +def test_on_start_writes_into_the_nested_object(): + json_serialization_writer = JsonSerializationWriter() + json_serialization_writer.on_start_object_serialization = clear_is_active + team = Team(lead=User(mobile_phone="+1 234 567 891")) + json_serialization_writer.write_object_value(None, team) + content = json_serialization_writer.get_serialized_content() + assert json.loads(content) == {"lead": {"is_active": None, "mobile_phone": "+1 234 567 891"}}