Skip to content

Commit 22de38a

Browse files
fix(providers/standard): remove premature param value validation in HITLOperator
HITLOperator params are form fields filled by a human at runtime. Calling `self.params.validate()` in `__init__` incorrectly validates param values at DAG parse time, before any human input exists. This causes `ParamValidationError` for any param without an explicit default value, breaking DAG import. The fix removes `self.params.validate()` from `validate_params()`. Value validation (type, required fields, schema) already happens correctly in `validate_params_input()` after the human submits the form. The `_options` key check is preserved — it is a structural constraint, not a value check. Closes #59551 Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 0dc4d33 commit 22de38a

2 files changed

Lines changed: 59 additions & 24 deletions

File tree

  • providers/standard

providers/standard/src/airflow/providers/standard/operators/hitl.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,15 @@ def validate_params(self) -> None:
147147
"""
148148
Validate the `params` attribute of the instance.
149149
150+
Note: Value validation (e.g., required fields, schema) is intentionally skipped here
151+
because HITLOperator params represent form fields that are filled by a human at runtime.
152+
Values do not exist at DAG parse time, so validating them in ``__init__`` would cause
153+
a ``ParamValidationError`` for any param without a default. Value validation happens
154+
in ``validate_params_input`` after the human submits the form.
155+
150156
Raises:
151-
ValueError: If `"_options"` key is present in `params`, which is not allowed.
157+
ValueError: If ``"_options"`` key is present in ``params``, which is not allowed.
152158
"""
153-
self.params.validate()
154159
if "_options" in self.params:
155160
raise ValueError('"_options" is not allowed in params')
156161

providers/standard/tests/unit/standard/operators/test_hitl.py

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -139,37 +139,67 @@ def test_validate_options_with_empty_options(self) -> None:
139139
params=ParamsDict({"input_1": 1}),
140140
)
141141

142-
@pytest.mark.parametrize(
143-
("params", "exc", "error_msg"),
144-
(
145-
(ParamsDict({"_options": 1}), ValueError, '"_options" is not allowed in params'),
146-
(
147-
ParamsDict({"param": Param("", type="integer")}),
148-
ParamValidationError,
149-
(
150-
"Invalid input for param param: '' is not of type 'integer'\n\n"
151-
"Failed validating 'type' in schema:\n"
152-
" {'type': 'integer'}\n\n"
153-
"On instance:\n ''"
154-
),
155-
),
156-
),
157-
)
158-
def test_validate_params(
159-
self, params: ParamsDict, exc: type[ValueError | ParamValidationError], error_msg: str
160-
) -> None:
161-
# validate_params is called during initialization
162-
with pytest.raises(exc, match=error_msg):
142+
def test_validate_params_rejects_options_key(self) -> None:
143+
"""_options is a reserved key and must not be allowed in params."""
144+
with pytest.raises(ValueError, match='"_options" is not allowed in params'):
163145
HITLOperator(
164146
task_id="hitl_test",
165147
subject="This is subject",
166148
options=["1", "2"],
167149
body="This is body",
168150
defaults=["1"],
169151
multiple=False,
170-
params=params,
152+
params=ParamsDict({"_options": 1}),
171153
)
172154

155+
def test_param_without_default_does_not_raise_on_init(self) -> None:
156+
"""Regression test for #59551.
157+
158+
HITLOperator params are form fields filled by a human at runtime. A param with no
159+
default value is valid — the human provides the value when submitting the form.
160+
Validating param values in __init__ incorrectly raises ParamValidationError at DAG
161+
parse time, before any value exists.
162+
"""
163+
# Must not raise ParamValidationError
164+
op = HITLOperator(
165+
task_id="hitl_test",
166+
subject="This is subject",
167+
options=["1", "2"],
168+
body="This is body",
169+
defaults=["1"],
170+
multiple=False,
171+
params={"my_param": Param(type="string")},
172+
)
173+
assert "my_param" in op.params
174+
175+
def test_param_with_default_does_not_raise_on_init(self) -> None:
176+
"""Params with explicit defaults continue to work normally."""
177+
op = HITLOperator(
178+
task_id="hitl_test",
179+
subject="This is subject",
180+
options=["1", "2"],
181+
body="This is body",
182+
defaults=["1"],
183+
multiple=False,
184+
params={"my_param": Param("hello", type="string")},
185+
)
186+
assert "my_param" in op.params
187+
188+
def test_param_with_wrong_value_type_does_not_raise_on_init(self) -> None:
189+
"""Value validation is deferred to validate_params_input at runtime, not __init__."""
190+
# Param("", type="integer") has a value that doesn't match the schema, but
191+
# this must NOT raise at init time — the human will provide the correct value at runtime.
192+
op = HITLOperator(
193+
task_id="hitl_test",
194+
subject="This is subject",
195+
options=["1", "2"],
196+
body="This is body",
197+
defaults=["1"],
198+
multiple=False,
199+
params={"param": Param("", type="integer")},
200+
)
201+
assert "param" in op.params
202+
173203
def test_validate_defaults(self) -> None:
174204
hitl_op = HITLOperator(
175205
task_id="hitl_test",

0 commit comments

Comments
 (0)