Skip to content

refactor(api): replace json.loads with Pydantic validation in repositories and tasks - #43704

Merged
asukaminato0721 merged 6 commits into
langgenius:mainfrom
haloneko:refactor/pydantic-json-repositories
Oct 9, 2026
Merged

asukaminato0721 merged 6 commits into
langgenius:mainfrom
haloneko:refactor/pydantic-json-repositories

Conversation

@haloneko

@haloneko haloneko commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Important

  1. Make sure you have read our contribution guidelines
  2. Ensure there is an associated issue
  3. Use the correct syntax to link this PR: Fixes #<issue number>.

Summary

Replace json.loads plus the manual dict handling around it with Pydantic validation, in the repositories and tasks layers.

  • repositories/data_source/credential_repository.py, repositories/knowledge/document_repository.py: drop the redundant validate_python(json.loads(x)) double parse in favour of validate_json(x).
  • repositories/upload_file_delivery_repository.py: replace cast(TenantCustomConfigDict, json.loads(x)) with a TypeAdapter that actually checks the shape.
  • tasks/enterprise_telemetry_task.py: json.loads + model_validate → model_validate_json.
  • tasks/process_tenant_plugin_autoupgrade_check_task.py: parse and validate the cached manifest in one pass; the adapter accepts None so a cached miss still returns None.
  • tasks/mail_human_input_delivery_task.py: _parse_recipient_payload validates the payload and returns a real RecipientType instead of a raw string. It used to forward a non-string email and an unknown TYPE unchecked; the caller already discarded those. New tests cover both shapes.
  • tasks/rag_pipeline/*: validate the queued payload as list[dict[str, Any]].

No other behaviour change is intended — each conversion keeps the same validation strength as the code it replaces.

Screenshots

Before After
N/A — backend-only refactor, no UI or observable behaviour change N/A

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've verified the change and added or updated tests where meaningful regression risk justifies coverage.
  • I've updated the documentation accordingly.
  • I ran make lint && make type-check (backend) and vp staged (frontend) to appease the lint gods
    • Backend: make is unavailable on Windows, so the underlying commands were run directly — ruff check ./api, ruff format --check ./api, pyrefly check — all clean.
    • Frontend: no frontend files changed, so vp staged was not run.

Part of #33092

From CodeBuddy

…te_json

_source_mapping and _mapping already validated through a TypeAdapter, but they decoded the string with json.loads first and then validated the resulting object. validate_json performs the decode and the validation in one pass; malformed input raises ValidationError, which the existing except clause already covers, so behaviour is unchanged.

Part of langgenius#33092
get_workspace_logo cast the json.loads result straight to TenantCustomConfigDict, which asserted the shape without checking it. TypeAdapter.validate_json decodes and validates in one pass.

The TypedDict is total=False, so partial configurations keep working, and this function only reads replace_webapp_logo, so ignoring any other stored key is not observable here.

Part of langgenius#33092
…e them

Both tasks decoded the payload with json.loads and immediately passed the result to a Pydantic model. model_validate_json and validate_json do the decode and the validation in one pass:

- enterprise_telemetry_task: TelemetryEnvelope.model_validate_json
- process_tenant_plugin_autoupgrade_check_task: the adapter accepts MarketplacePluginSnapshot | None, because a cached miss is stored as JSON null and must keep returning None instead of falling through to False

Part of langgenius#33092
_parse_recipient_payload read two keys out of a raw dict and handed the TYPE string back as a RecipientType, so a payload carrying an unknown channel or a non-string email reached the caller unchecked. A model validates both fields in one pass and returns a real enum.

The caller already skipped anything that was not EMAIL_MEMBER or EMAIL_EXTERNAL, so unusable payloads still end up as a skipped recipient. The helper had no tests at all, so cover both the accepted and the rejected shapes.

Part of langgenius#33092
Both queue tasks decoded the uploaded payload with json.loads and then iterated it, so a payload that was not a list of objects only failed later inside a worker thread. TypeAdapter(list[dict[str, Any]]) checks the shape at the task boundary; RagPipelineTaskProxy already writes exactly that shape.

Entries stay plain mappings because run_single_rag_pipeline_task still validates each one into RagPipelineInvokeEntity in its own thread context.

Part of langgenius#33092
@ghfind-review ghfind-review Bot added the review: medium ghfind author score; see https://ghfind.com label Oct 8, 2026
@github-actions github-actions Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Oct 8, 2026
@haloneko haloneko changed the title Refactor/pydantic json repositories refactor(api): replace json.loads with Pydantic validation in repositories and tasks Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Diff

base → PR
--- /tmp/pyrefly_base.txt	2026-10-08 13:32:48.337779963 +0000
+++ /tmp/pyrefly_pr.txt	2026-10-08 13:32:38.569721335 +0000
@@ -3860,6 +3860,8 @@
    --> tests/unit_tests/core/moderation/api/test_api.py:193:78
 ERROR Argument `Session` is not assignable to parameter `session` with type `scoped_session[Unknown]` in function `core.moderation.api.api.ApiModeration._get_api_based_extension` [bad-argument-type]
    --> tests/unit_tests/core/moderation/api/test_api.py:196:76
+ERROR Argument `Literal['invalid']` is not assignable to parameter `config` with type `dict[str, Any] | None` in function `core.moderation.base.Moderation.__init__` [bad-argument-type]
+    --> tests/unit_tests/core/moderation/test_sensitive_word_filter.py:1278:92
 ERROR Method `trace` inherited from class `BaseTraceInstance` has no implementation and cannot be accessed via `super()` [missing-attribute]
   --> tests/unit_tests/core/ops/test_base_trace_instance.py:22:9
 ERROR Argument `() -> Session` is not assignable to parameter `session_factory` with type `sessionmaker[@_]` in function `sqlalchemy.orm.scoping.scoped_session.__init__` [bad-argument-type]

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 70.85% 70.72% -0.13%
Strict coverage 70.51% 70.38% -0.13%
Typed symbols 56,557 56,457 -100
Untyped symbols 23,383 23,486 +103
Modules 3709 3709 0

@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.69%. Comparing base (593f051) to head (cff8cdc).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #43704      +/-   ##
==========================================
- Coverage   88.69%   88.69%   -0.01%     
==========================================
  Files        5191     5191              
  Lines      326420   326421       +1     
  Branches    65424    65423       -1     
==========================================
- Hits       289512   289505       -7     
- Misses      31681    31689       +8     
  Partials     5227     5227              
Flag Coverage Δ
api 88.40% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@haloneko

haloneko commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

@asukaminato0721 Ready for review. This is the repositories/ + tasks/ slice of #33092 — replaces json.loads with Pydantic validation, no behaviour changes intended. 65 tests pass, lint clean. PTAL when you get a chance. Thanks!

Comment thread api/tests/unit_tests/tasks/test_mail_human_input_delivery_task.py
Comment thread api/tests/unit_tests/tasks/test_mail_human_input_delivery_task.py
@asukaminato0721 asukaminato0721 self-assigned this Oct 9, 2026
@asukaminato0721
asukaminato0721 added this pull request to the merge queue Oct 9, 2026
Merged via the queue into langgenius:main with commit 1e3fe6b Oct 9, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review: medium ghfind author score; see https://ghfind.com size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants