Split persistence and report JSON serialization (+introduce Pydantic) - #171
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR separates durable persistence JSON from human-facing report JSON, and begins migrating JSON boundaries to Pydantic v2 models (notably AnalysisOptions and AnalyzedSeries).
Changes:
- Make change-point domain
to_json()output precise persistence JSON (numeric types preserved,qhatincluded) and remove the oldrounded=switch. - Move rounded/stringified JSON formatting into
Report’s JSON renderer. - Introduce Pydantic v2 models for serialization (
AnalysisOptionsModel,AnalyzedSeriesModel) and add tests for JSON-boundary round-trips and validation behavior.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Adds Pydantic and its transitive dependencies to the locked environment. |
| pyproject.toml | Declares pydantic>=2,<3 as a runtime dependency. |
| otava/serialization.py | Introduces Pydantic v2 models that define/validate persistence JSON shapes. |
| otava/series.py | Migrates AnalysisOptions to Pydantic, updates analyzed-series persistence JSON (typed metrics/data/options), and restores options via model validation. |
| otava/change_point_divisive/base.py | Removes rounded JSON paths, makes change-point JSON precise, and includes qhat in serialized change points. |
| otava/report.py | Implements report-specific rounded/stringified JSON rendering for human output. |
| tests/change_point_classes_test.py | Updates expectations to match precise persistence JSON and qhat inclusion. |
| tests/series_test.py | Adds JSON-boundary and Pydantic validation tests for options/analyzed-series persistence. |
| tests/report_test.py | Updates expected report JSON shape/format and refactors some test construction. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Gerrrr
force-pushed
the
pydantic-and-split-persistence-report-json
branch
from
August 19, 2026 18:37
4579c54 to
21edff8
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (4)
otava/serialization.py:67
- Top-level series attributes are also not guaranteed to be strings:
GraphiteImporterstoresNonefor unmatched events, while database importers copy attribute column values without conversion (otava/importer.py:573-575and799-801). Pydantic therefore rejects previously JSON-serializable series at this new validation boundary. Model these lists with nullable/JSON-scalar values rather thanstronly.
attributes: Dict[str, List[str]]
otava/serialization.py:62
- Postgres and BigQuery importers explicitly build
List[float]timestamps withdatetime.timestamp()(otava/importer.py:546-560and772-786). Timestamps with microseconds therefore contain a fractional part, which Pydantic cannot validate asint, soAnalyzedSeries.to_json()now raises instead of persisting those series. Preserve both integer-indexed and fractional Unix times.
time: List[int]
otava/serialization.py:54
- Change-point group attributes can be
None:GraphiteImporterinsertsNonewhen no event matches a timestamp (otava/importer.py:169-180), and those values flow into groups viaSeries.attributes_at(). This strictstrtype makesAnalyzedSeries.to_json()fail whenever such a timestamp has a change point. Allow nullable/JSON-scalar attribute values so valid importer output remains persistable.
This issue also appears on line 67 of the same file.
attributes: Dict[str, str]
otava/series.py:450
- Neither the structured nor legacy metric branch actually restores
unit:Metric.__init__unconditionally assignsself.unit = ""atotava/series.py:55, ignoring the third argument passed here. Consequentlyfrom_json()silently loses any persisted unit and the nextto_json()emits an empty value. Update the constructor to retain itsunitargument and cover the metric round trip.
metric_json.get("unit", ""),
)
else:
new_metrics[metric_name] = Metric(None, None, metric_json)
Sowiks
approved these changes
Aug 24, 2026
Make domain to_json() methods return the precise persistence representation: ChangePoint and ChangePointGroup now emit numeric fields, include qhat, and no longer use rounded=True/False as a presentation switch. Move rounded JSON output into Report, alongside the other report renderers. ReportType.JSON now owns display-only fields such as backward_change_percent and formats human-facing values as strings. Add Pydantic v2 schema models for the persisted analyzed-series payload and validate AnalyzedSeries.to_json() against that schema before returning JSON-safe data. Update tests to pin the split: persistence JSON stays numeric and round-trippable, while report JSON keeps the existing rounded output shape.
Make AnalysisOptions inherit from the Pydantic v2 AnalysisOptionsModel, with defaults defined on the model itself. Reject unknown option fields and validate assignment so deserialized and CLI-populated options use the same typed contract. Use model_dump(mode="json") when serializing analyzed-series options, and rebuild options in from_json() with AnalysisOptions.model_validate(...) instead of manual field copying. Keep the scope narrower than PR #93: reuse the Pydantic direction, but avoid its broader unrelated model conversions and old Pydantic v1 APIs.
Parse change point timestamps back to datetime objects when loading analyzed series from JSON-decoded payloads, so runtime state matches direct in-memory round trips. Also replace the ambiguous report JSON float formatter with explicit six-decimal formatting while preserving the existing report representation.
Allow nullable JSON scalar attributes, preserve integer and fractional timestamps, and retain metric units across current and legacy JSON round trips.
Gerrrr
force-pushed
the
pydantic-and-split-persistence-report-json
branch
from
August 24, 2026 04:25
4cefd6b to
73d0fd9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR separates durable JSON serialization from report formatting (a follow-up to #161) and starts the Pydantic migration from #78.
The first commit makes
to_json()mean “precise persistence JSON” for the change-point domain objects. Numeric values now stay numeric,qhatis included in serialized change points, and the oldrounded=True/Falseswitch is removed from the domain model. Rounded, human-facing JSON output now belongs toReport, alongside the existing log and regressions-only renderers.The second commit adds a real JSON boundary test for analyzed-series persistence. It verifies that
AnalyzedSeries.to_json()survivesjson.dumps()/json.loads()before being passed back intoAnalyzedSeries.from_json().The third commit converts
AnalysisOptionsto a Pydantic v2 model. Defaults now live on the model, unknown fields are rejected, assignment is validated, andAnalyzedSeries.from_json()restores options withAnalysisOptions.model_validate(...)instead of manual field copying. This follows the direction of the closed #93, but keeps the scope tighter and uses Pydantic v2 APIs.