From 8bdb49488dc2afd7c737703fefca7eaee8548f2d Mon Sep 17 00:00:00 2001 From: its-amann Date: Thu, 5 Nov 2026 13:10:10 +0530 Subject: [PATCH] FIX Preserve required parameters after a wire round trip --- pyrit/models/parameter.py | 14 ++++++++------ tests/unit/models/test_parameter.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/pyrit/models/parameter.py b/pyrit/models/parameter.py index 6a36fee733..351cc7f245 100644 --- a/pyrit/models/parameter.py +++ b/pyrit/models/parameter.py @@ -169,27 +169,29 @@ class Parameter(BaseModel): @classmethod def _reconstruct_param_type_from_wire(cls, data: Any) -> Any: """ - Rebuild the live ``param_type`` when validating from a serialized payload. + Restore live parameter metadata from a serialized payload. Serialization drops the live ``param_type`` and projects it onto the display fields ``type_name`` / ``choices`` / ``is_list``. A client that deserializes the wire form (e.g. the CLI consuming the REST catalog) has those fields but no live type; this reconstructs a coercion-capable ``param_type`` from them so the round-tripped ``Parameter`` can still - coerce and validate values. In-process construction (which already - supplies a live ``param_type``, or supplies neither) is left untouched. + coerce and validate values. The wire form also distinguishes a required + default from an optional null default using the ``required`` flag. Returns: - Any: The input unchanged, or a copy with ``param_type`` reconstructed - from the serialized display fields. + Any: The input unchanged, or a copy with live metadata restored. """ if not isinstance(data, dict): return data needs_param_type = data.get("param_type") is None and "type_name" in data needs_reference = data.get("reference") is None and data.get("reference_type") is not None - if not needs_param_type and not needs_reference: + needs_required_default = data.get("required") is True and data.get("default") is None + if not (needs_param_type or needs_reference or needs_required_default): return data data = dict(data) + if needs_required_default: + data["default"] = REQUIRED_VALUE if needs_param_type: data["param_type"] = _param_type_from_display( type_name=data.get("type_name"), diff --git a/tests/unit/models/test_parameter.py b/tests/unit/models/test_parameter.py index bcd95c0905..62fd60779e 100644 --- a/tests/unit/models/test_parameter.py +++ b/tests/unit/models/test_parameter.py @@ -266,6 +266,16 @@ def test_required_default_serializes_to_none(self) -> None: assert dumped["type_name"] == "str" assert dumped["choices"] == ["a", "b"] + @pytest.mark.parametrize("default", [REQUIRED_VALUE, None]) + def test_required_status_round_trips(self, default: object) -> None: + original = Parameter(name="count", description="d", param_type=int, default=default) + + restored = Parameter.model_validate_json(original.model_dump_json()) + + assert restored.required is original.required + assert restored.default is default + assert restored.coerce_value("5") == 5 + def test_enum_default_serializes_to_member_value(self) -> None: dumped = Parameter(name="speed", description="d", default=_Speed.FAST, param_type=_Speed).model_dump()