Skip to content

refactor(api): replace json.loads with Pydantic validation in retention and website layers- #43588 - #44170

Open
haloneko wants to merge 8 commits into
langgenius:mainfrom
haloneko:refactor/pydantic-inspector-message
Open

haloneko wants to merge 8 commits into
langgenius:mainfrom
haloneko:refactor/pydantic-inspector-message

Conversation

@haloneko

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

Replaces json.loads plus handwritten field checks with Pydantic validation at six call sites, so manifest / index / cache shapes and field types are validated in one pass instead of by ad-hoc isinstance and set-difference checks. Validation reuses the TypedDicts the code already declares, so each validated shape has a single source of truth.

The four logical changes span seven commits so each can be reverted independently:

  • delete_archived_workflow_run — validated with a TypeAdapter over the existing _ArchiveManifest TypedDict; its six fields already match the previous required_fields set exactly.
  • bundle_archive_maintenance — the handwritten 13-field check is replaced by validation against the existing BundleManifest TypedDict. BundleManifest declares 18 fields but only 13 are required; the five range/audit fields are absent from older manifests, so they stay optional (NotRequired) to preserve the current strictness rather than tightening it.
  • archive_paid_plan_workflow_run — shard index.json (validated against ArchiveBundleIndexDict) plus the manifest scan in _build_bundle_index (validated against ShardManifestEntryModel). The manifest model requires only the fields actually read, so older manifests still contribute to a rebuilt index.
  • message_export_service / website_service — _MessageMetadataModel reads only the fields the export uses; cached Firecrawl payloads are validated against the existing FirecrawlDocumentData TypedDict.
    Two follow-up commits (simplify validation adapters per review, drop redundant casts by validating existing TypedDicts) drop the throwaway BaseModel wrappers and the cast() / model_dump() round-trips in favour of the existing TypedDicts, and a test-only commit (test(api): cover the bundle manifest branch without campaign_id) closes the remaining branch coverage.

api/services/retention/workflow_run/archive_bundle_index.py still decodes manifests with json.loads; it is intentionally out of scope for this PR.

Business rules that depend on call arguments (schema/format values, tenant and prefix matching, run_ids length, per-table object_key) stay in Python, since Pydantic cannot see them. Validation targets the existing total/NotRequired TypedDicts, so validated results are only read through declared fields; unknown keys are dropped, which is behaviour-neutral at these call sites. ShardManifestEntryModel keeps extra="allow" because it is consumed as a model rather than a plain dict, and _MessageMetadataModel uses extra="ignore" deliberately.

Tests

  • Added 16 tests across the five touched test files, covering the new validation boundaries — including one that pins the five optional manifest fields as optional so they cannot be silently made mandatory later.
  • archive_paid_plan_workflow_run had no test file at all; added one.
  • passed over api/tests/unit_tests/services/retention + api/tests/unit_tests/services/test_website_service.py.
  • ruff check ./api and ruff format --check ./api clean; pyrefly reports 0 diagnostics for the changed files.

From CodeBuddy

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

Part of #33092

…ived_workflow_run

Replace json.loads plus the handwritten required-field/tables checks with a TypeAdapter over the existing _ArchiveManifest TypedDict, so manifest shape and field types are validated in one pass.

Business rules that depend on call arguments (schema_version, workflow_run_id, tenant_id, app_id, workflow_id matching and the archived table list) stay in Python, since Pydantic cannot see them.

Part of langgenius#33092
…chive_maintenance

Replace json.loads plus the handwritten required-field and table-entry checks with a Pydantic model, so manifest shape and field types are validated in one pass.

BundleManifest declares eighteen fields but only thirteen are required: the five range/audit fields are absent from older manifests, so they stay optional to keep the current validation strictness instead of tightening it. Unknown fields are preserved via extra='allow' to match the previous json.loads behaviour.

Business rules that depend on the object_prefix argument (schema/format values, prefix match, tenant prefix, run_ids length, per-table object_key) stay in Python.

Add tests pinning the mandatory/optional split and the new type validation.

Part of langgenius#33092
…id_plan_workflow_run

Replace json.loads plus the handwritten dict checks in _load_bundle_index and _build_bundle_index with Pydantic models.

The shard index model mirrors ArchiveBundleIndexDict; campaign_ids defaults to an empty list rather than None because callers do set(index.get('campaign_ids', [])) and set(None) would raise. The manifest model requires only the four fields _build_bundle_index actually reads, so older manifests that lack the later range/audit fields still contribute to a rebuilt index.

Business rules that depend on identity (schema/format values and shard prefix match) stay in Python.

This module had no unit tests; add one covering both entry points.

Part of langgenius#33092
…ydantic

Replace the last two json.loads calls in the claimed scope with Pydantic validation.

website_service: FirecrawlDocumentData is a total TypedDict, but cached payloads were written before some of its fields existed, so a model with None defaults is used instead of rejecting them. Documents are dumped with exclude_unset so the returned dict matches the cached payload rather than gaining null entries.

message_export_service: only 
etriever_resources is read from message_metadata, so the model declares just that field and ignores the rest. The tolerant except is kept as (ValidationError, TypeError) because a non-string column must still yield an empty list rather than fail the export.

Part of langgenius#33092
- Type retriever resources as list[dict[str, JsonValue]] instead of list[Any], matching how installed_app_message_service already handles the same message_metadata payload.

- Drop the TypeAdapter wrappers whose target is a BaseModel: ArchiveBundleIndexModel, ShardManifestEntryModel and _MessageMetadataModel can all call model_validate_json directly. TypeAdapter stays only where the target is not a BaseModel, i.e. the _ArchiveManifest TypedDict and list[_CachedFirecrawlDocument].

Part of langgenius#33092
Validate the V2 bundle manifest, the shard index and the cached Firecrawl documents directly against the TypedDicts the code already declares, instead of routing through throwaway BaseModel classes and casting the result back:

- remove three cast() calls together with the matching model_dump() round-trips, which produced a plain dict[str, Any] and erased the type information
- drop the now-unused TypeVar imports
- reuse the existing BundleManifest, ArchiveBundleIndexDict and FirecrawlDocumentData definitions, so the validated shape has a single source of truth
- keep the five range/audit manifest fields NotRequired, so validation stays as permissive as the handwritten check it replaces

Rewrite the affected test fixtures so they match the shapes the code actually writes and reads: fill in the range/audit fields of the bundle manifest fixture, add the description and markdown fields to the stored Firecrawl document fixture, and rename the optional-range-field test accordingly.

Part of langgenius#33092
The false side of the campaign_id check in _build_bundle_index was never taken: the happy-path test supplies a campaign_id and the error-path tests raise before reaching it, so Codecov counted the branch against the patch.

Part of [langgenius#33092](langgenius#33092)
@ghfind-review ghfind-review Bot added the review: medium ghfind author score; see https://ghfind.com label Oct 15, 2026
@github-actions github-actions Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 74.80% 74.80% +0.00%
Strict coverage 74.47% 74.47% +0.00%
Typed symbols 60,985 60,999 +14
Untyped symbols 20,638 20,641 +3
Modules 3761 3762 +1

@codecov

codecov Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.94%. Comparing base (f7840ab) to head (f58a7ee).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #44170      +/-   ##
==========================================
+ Coverage   88.93%   88.94%   +0.01%     
==========================================
  Files        5287     5287              
  Lines      324064   324054      -10     
  Branches    63702    63687      -15     
==========================================
+ Hits       288192   288226      +34     
+ Misses      30694    30662      -32     
+ Partials     5178     5166      -12     
Flag Coverage Δ
api 88.61% <100.00%> (+0.03%) ⬆️

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.

@github-actions github-actions Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Oct 15, 2026
@haloneko
haloneko marked this pull request as draft October 15, 2026 14:51
- use contextlib.suppress for the message_metadata validation guard (ruff suppressible-exception)
- replace the net-new Mock/MagicMock/SimpleNamespace test doubles with real in-memory storage and stub classes (check_no_new_test_doubles)
@haloneko
haloneko force-pushed the refactor/pydantic-inspector-message branch from f58a7ee to b3804cf Compare October 15, 2026 14:59
@haloneko
haloneko marked this pull request as ready for review October 15, 2026 15:01
@haloneko

Copy link
Copy Markdown
Contributor Author

@asukaminato0721 could you please review this PR? Thanks!

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:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant