Skip to content

fix(config): coerce null object config sections to their defaults#3573

Open
ly-wang19 wants to merge 1 commit into
bytedance:mainfrom
ly-wang19:fix/config-null-object-sections
Open

fix(config): coerce null object config sections to their defaults#3573
ly-wang19 wants to merge 1 commit into
bytedance:mainfrom
ly-wang19:fix/config-null-object-sections

Conversation

@ly-wang19

Copy link
Copy Markdown
Contributor

Why

#3434 made commented-out list sections (models / tools / tool_groups) parse as [] instead of crashing — but the same scenario still crashes for object sections. Commenting out every key under e.g. memory:, summarization:, guardrails:, tool_output: or run_events: makes PyYAML parse the value as None, and AppConfig then raises an opaque Input should be a valid dictionary for that section. This breaks the documented cp config.example.yaml config.yaml first-run flow (the exact flow #3434 fixed), since config.example.yaml ships those object sections and a user disabling one by commenting out its body hits the crash.

Repro on main: a config containing memory: (null) fails to load with a ValidationError.

What changed

No change for valid configs. A present-but-null section now falls back to its default instead of crashing: a model_validator(mode="before") drops None-valued top-level sections so each field uses its default (list sections → [] via default_factory, object sections → their default config). This generalizes the previous list-only field_validator (now removed as subsumed) and the database special-case in from_file to every section. Required sections with no default (sandbox) still error when null — there is nothing to fall back to.

Surface area

Internal config loading (packages/harness/deerflow/config/app_config.py) — no frontend, endpoint, agent, sandbox, skills, or dependency change; robustness fix for malformed/partial config.yaml.

Bug fix verification

  • Test: backend/tests/test_app_config_reload.py::test_app_config_coerces_commented_out_object_sections.
  • Red on main / green on branch: yes — on main, AppConfig.from_file of a config with a null object section raises ValidationError; the new test reproduces that and passes on this branch. The existing test_app_config_coerces_commented_out_list_sections (fix(config): coerce null config.yaml list sections to empty list #3434) still passes, confirming the generalization preserves list-section behavior.

Validation

In backend/:

  • uv run pytest tests/test_app_config_reload.py12 passed.
  • uv run ruff check + uv run ruff format --check on the changed files → clean.

AI assistance

Tool(s) used: Claude Code

How: Used to spot that #3434's null-section handling was list-only, reproduce the object-section crash, generalize the coercion via a model_validator, and add the regression test. I reviewed every line and verified locally with the test + ruff runs above.

  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

bytedance#3434 made commented-out list sections (models/tools/tool_groups) parse as []
instead of crashing, but the same scenario still crashes for object sections:
commenting out every key under e.g. memory: / summarization: / guardrails:
makes PyYAML parse the value as None, and AppConfig then raises "Input should
be a valid dictionary" for that section — breaking the documented
`cp config.example.yaml config.yaml` first-run flow.

Generalize the handling: a model_validator(mode="before") drops None-valued
sections so each field falls back to its default (list sections -> [], object
sections -> their default config). This subsumes the previous list-only
field_validator and the database special-case. Required sections without a
default (sandbox) still error when null.

Adds test_app_config_coerces_commented_out_object_sections; the existing
list-section regression test still passes.
@github-actions github-actions Bot added area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/S PR changes 20-100 lines labels Jun 14, 2026

@fancyboi999 fancyboi999 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the full change against the field definitions — it's a correct, well-scoped generalization of the #3434 list-only fix, and the broadening to all sections is safe:

  • _drop_null_config_sections (app_config.py:161) swaps the old 3-field field_validator for a model_validator(mode="before") that strips every present-but-null top-level key, so each section falls back to its default. For the object sections in the bug (memory / summarization / guardrails / tool_output / run_events, all default_factory=...) a null value now yields the default config instead of the opaque Input should be a valid dictionary. Matches the documented cp config.example.yaml config.yaml flow that #3434 set out to keep working.
  • I specifically checked the broadening doesn't regress the Optional sections: checkpointer (:144) and stream_bridge (:151) are the only | None top-level fields, and both are Field(default=None) — so dropping a null key resolves to None, identical to the previous | None parsing. No section silently changes value.
  • sandbox (:99) has no default, so a null sandbox: still errors — the message just shifts from Input should be a valid dictionary to Field required (the docstring calls this out). Still fails closed, and the test wisely supplies a valid sandbox so it doesn't trip on this.
  • model_config = ConfigDict(extra="allow") means dropped unknown-null keys are a non-issue — they were only ever kept as None extras anyway.

Test covers the object-section case end-to-end through from_file, asserting each null section becomes a non-None default.

One forward-looking, non-blocking note: this now bakes in an invariant that no top-level field treats null as a meaningful value distinct from its default — true today, since the only Optional fields default to None. If a future section is ever added as X | None = Field(default=<non-None>) where a user's explicit null should mean "disabled/None", this validator would silently override it with the non-None default. Might be worth a one-line comment on the validator capturing that assumption so it's not a surprise later.

Looks good to merge. (Reviewed at code level; I didn't run the suite locally.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/S PR changes 20-100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants