Model the per-post stats response against the wp.com source - #1526
Conversation
The helper mapped every boolean to `None`, including `true`. Its siblings all reject `true` explicitly — `deserialize_false_or_string`, `deserialize_false_or_string_or_null` and `deserialize_u64_or_none` — so a value the API is not expected to send was being silently swallowed here alone. A test case pinned the divergence. The doc also claimed a missing field yields `None`. Serde does not call `deserialize_with` for an absent key, so without `#[serde(default)]` a missing field is a hard error. The new test asserts both halves of that. Changes: - Match `Bool(false) | Null` for `None` and error on `Bool(true)` - Document the `#[serde(default)]` requirement for omitted fields - Move the `true` case into a new `_errors` test alongside the malformed-value case - Add a test covering a missing field with and without `default`
Checking the module against `class.wpcom-json-api-stats-post-views-v1-1-
endpoint.php` and `stats_get_post()` turned up types that overstate what
the API sends, and docs that describe fields it doesn't have.
Address the home page as `StatsPostTarget` rather than `PostId(0)`. Zero
is not a valid `PostId` anywhere else in the crate, and what the API
counts for it is not obvious enough to leave in a doc comment.
`stats_utm` already takes a typed path param this way.
`average`, `averages.overall` and `averages.months` arrive as integers —
PHP casts all three before sending, and FluxC has modelled them as `Int`
for years. Only `change` is a genuine float.
Changes:
- Add `StatsPostTarget`, with `Post { id }` and `HomePage` variants, and
take it as the endpoint's path argument
- Type the three average fields as `u64`
- Type `post_author` as `UserId` and `post_date_gmt` as `WpGmtDateTime`
- Add `post_modified_gmt` and `post_excerpt`, both sent but unmodelled
- Serialize the response back into the `fields`/`data` column table it
parses from, so a serialized response can be read again
- Correct `highest_week_average`, which is the highest single-day count
of recent weeks rather than a weekly average, and `highest_day_average`,
which is a monthly average of daily views
- Document that the home page's figures cover the whole site when the
front page is static, and that `post` is then `null` rather than `false`
- Document that `daily_views` is also empty for a never-viewed target,
whose history the API replaces with one unusable placeholder row, and
cover that row in the column-handling test
- Give `post` a `serde` default, matching the two sibling fields
- Make the fixtures' weeks self-consistent: only the current week is
partial, `average` follows from the days, and `isInfinity` only follows
a zero week
- Name the e2e trials `post::` to match the other stats trials, and
resolve the borrowed post id with `try_from`
Captured four responses from a live site to settle two claims the source alone couldn't. Both were wrong in the module, in opposite directions. `stats_get_post()` does attach a `permalink`, and it reaches the wire — it is the 25th key on the post row, after `filter`. The doc comment asserted the response carried no permalink, and the field was unmodelled, so callers had no way to reach the post's URL. A never-viewed post does not get the API's no-history fallback row. Its daily history is padded from the publication date to today with integer zeros, so `daily_views` is populated, not empty. What does change is `years` and `averages`: with no view to anchor on, the API reports every year from 1970 to the present, each with an empty month map — 57 entries for a post published last year. `post-no-views.json` described neither shape. It carried a single 2026 year, which no response can produce. Changes: - Add `StatsPostDetails::permalink`, and point `guid` at it - Note that the post row carries no featured image - Replace the `daily_views` emptiness note with the padding behaviour - Document the 1970 year range on `years` - Rebuild `post-no-views.json` from a real never-viewed response, and rename its test to match what it covers - Keep the fallback-row case in the column-handling test, relabelled: the server can still emit it, but it is not the never-viewed path
Comments and tests that explained serde or documented what the types used to be, rather than what the endpoint sends. Changes: - Drop the `#[serde(default)]` guidance from `deserialize_false_as_none` and the test asserting serde's missing-field behaviour - Drop `#[serde(default)]` from the response's `post` field; the endpoint always sends it - Restore the fixtures' weeks and the assertions over them, and add only the three fields the post row gained - Restore the e2e comments and the `homepage` trial name - Cut the daily-view case for the API's no-history fallback row, which is not a shape the endpoint was seen to send - Cut the notes on how `permalink` is derived and on the absent featured image - Use `fmt::Display` rather than a fully qualified path
`post_stats::` says what the trials cover; `post::` names a noun and sits next to `top_posts::`, where it reads like a posts endpoint rather than a stats one. The other stats trials drop their `stats_` prefix because what remains still describes the endpoint, which isn't true here. Changes: - Restore the `post_stats::` trial prefix - Restore the `as i64` cast on the borrowed post id
XCFramework BuildThis PR's XCFramework is available for testing. Add to your .package(url: "https://github.com/automattic/wordpress-rs", branch: "pr-build/1526")Built from 581c207 |
|
@oguzkocer Thanks for tackling this! I'll approve this to unblock it, but Claude's review did find some issues, and the first one involving review-pr-1526-per-post-stats-2026-08-08.pdf Also, I noted the "PostId(0) doesn't always mean the home page" remark and wasn't sure what it meant - does the client need to change how it gets stats for the home page? |
Changes: - Report the rejected `true` with `invalid_value`, as the visitors in `numeric.rs` do, rather than a custom message - Use `//` for the note above the two column consts; as a doc comment it attached to `PERIOD_COLUMN` alone
The test compared only `daily_views`, so it covered the field the column table flattens into and nothing else. Re-serializing the reparsed value and comparing covers every field without needing `PartialEq` on the record. Runs over all three fixtures. The home page is the case worth having: its `post` arrives as boolean `false`, becomes `None`, and serializes as `null`, so it is the only fixture where the round trip changes the wire shape. Compares `serde_json::Value` rather than the serialized strings, since the response holds `HashMap`s and two maps built from the same JSON do not iterate in the same order.
`years` documents that a target with no views gets an entry for every year from 1970, and the fixture carries them, but nothing asserted it.
`permalink` is derived per request rather than read from a column, so it is the field in the post row most likely to move. Requiring it meant an unexpected shape cost the whole response, for a URL the response is still useful without. Changes: - Type `permalink` as `Option<String>`, read through `deserialize_false_as_none` with a serde default
`/stats/top-posts` reports the home page as a pseudo-entry with id 0, so callers feeding those ids into this endpoint have to know what 0 means. The conversion puts the rule in one place. Changes: - Add `impl From<PostId> for StatsPostTarget`, mapping the home page id to `HomePage` - Name the id as `HOME_PAGE_POST_ID` rather than repeating the literal
`post_author` on a WordPress.com site carries the account's global id, which `WpComUserId` names. `UserId` is the wp.org site-scoped id. `deserialize_i64_or_string_as_t` had no `u64` counterpart, which `WpComUserId` needs. Changes: - Add `deserialize_u64_or_string_as_t` to `wp_serde_helper` - Type `StatsPostDetails::author_id` as `WpComUserId`
|
@nbradbury Thank for the review. I've addressed the ones that made sense and also updated the PR description to reflect some of the changes.
The documentation in the PR description seems somewhat clear and somewhat confusing at the same time: (shared as excerpt below)
I don't know if anything needs to be changed in WPAndroid as a result of this. I suggest having a conversation with Claude on this with the context you have related to WPAndroid implementation. |
* Add wp.com `GET /sites/<site_id>/stats/post/<post_id>` endpoint
Per-post view stats, for the "Latest Post Summary" card in the new
Jetpack app stats. The response carries the post's metadata, like count,
and comment count alongside the view history, so the card needs no
separate post fetch.
Notes from capturing real responses:
- The endpoint accepts no query params. `num`, `date`, and `period` are
silently ignored, so there is no params struct.
- A week's `change` is `null`, a number, or `{"isInfinity": true}` when
the previous week had no views. Modelled as `StatsPostViewsChange`,
which round-trips all three.
- `months` inside `years`/`averages` is `[]` rather than `{}` when empty,
and `post_author` arrives as a string.
`data` holds the post's entire history — thousands of rows for an old
post — so `recent_daily_views(days)` returns just the trailing window
callers actually render.
Verified against 60 real responses across 15 sites.
* Make `recent_daily_views` walk only the window it returns
It called `daily_views()` first, which materializes the post's entire
history — ~4,800 data points, each with a heap-allocated String, on an
old post — then sliced and copied the tail. That is the cost the method
exists to avoid.
It now resolves the column positions once, walks `data` in reverse, and
takes only the entries it returns. Both accessors share the row reader
via a small private `StatsPostViewsDataColumns`, and both bail early
when `fields` doesn't name the columns, matching `stats_visits`.
The FFI surface is unchanged — verified by diffing the generated Swift
declarations before and after.
Also documents why the post row's `comment_count` is dropped in favour
of `discussion.comment_count`, and records that the three `change` wire
shapes are what 60 real responses across 15 sites produced.
* Flatten the daily view history at parse time
The `daily_views`/`recent_daily_views` helpers were `#[uniffi::export]`
methods on a `uniffi::Record`, which generates callers like:
uniffi_wp_api_fn_method_..._recent_daily_views(
FfiConverterTypeStatsPostViewsResponse_lower(self), ...)
Every call lowered the whole response back into Rust — including the
`data` array, thousands of rows on an old post — so the helper cost more
than reading `data` natively. That is backwards from why it existed.
The `fields`/`data` column table is now flattened into a `daily_views`
field while deserializing, and both exported methods are gone. Callers
take a trailing window with `dailyViews.suffix(7)` / `takeLast(7)` — no
FFI at all. The column positions are still read from `fields` rather
than assumed; that just happens once now.
`StatsPostViewsDataValue` becomes private along with the raw shape, so
it no longer appears in the bindings.
Adds a test that reorders the `fields` columns, which nothing previously
covered.
* Rename `stats_post_views` to `stats_post`
The module didn't mirror its endpoint path. Fifteen of the eighteen stats
modules do; the three that don't each have a reason (`stats_summary` has
no segment to mirror, the location ones would be `stats_location_views_*`).
This one had no such excuse, and `_views` undersold the type — the
response also carries the like count, comment count, and post metadata,
which is precisely what makes the endpoint useful.
Two types needed more than the mechanical substitution:
- `StatsPostViewsPost` would have become `StatsPostPost`, so it is now
`StatsPostDetails`.
- `StatsPostViewsDataPoint` would have become `StatsPostDataPoint`, one
letter from the existing `StatsPostsDataPoint` in `stats_visits` (posts
published per period) and unrelated in meaning. UniFFI's namespace is
flat, so both would sit together in Swift and Kotlin autocomplete. It
is now `StatsPostDailyView`.
* Simplify the per-post stats module
Findings from four parallel cleanup reviews (reuse, simplification,
efficiency, altitude). Net -67 lines, no behaviour change and no change
to the generated FFI surface.
- Drop the private `RawStatsPostDataValue` in favour of the existing
public `StatsVisitsDataValue`. It was a character-for-character copy,
and `stats_subscribers` already imports that type cross-module for the
same purpose.
- `daily_views` now takes `data` by value and moves each row's period
string instead of cloning it, into a pre-sized `Vec`. The caller drops
the rows immediately afterwards, so the ~4,800 clones a long-lived
post incurred were pure waste.
- Replace `StatsPostChange`'s hand-written `Deserialize`/`Serialize`
with `#[serde(from, into)]` and two `From` impls — the idiom already
used for the response 150 lines above. Also retires an
`#[allow(dead_code)]`.
- Tests call `daily_views` directly rather than parsing a 26-line JSON
envelope to reach it, collapsing three near-identical tests into one
`rstest`. Drops a redundant test whose assertions were all made more
precisely elsewhere.
- The e2e trial resolves its post id inside the closure rather than
during collection, so unrelated e2e runs no longer pay a serial
network round trip per site.
Known follow-up, deliberately not done here: the column-table lookup now
exists three times (`stats_visits`, `stats_subscribers`, `stats_post`)
and wants a shared helper. That edits two modules outside this branch.
* Support home page stats (`PostId(0)`)
`/stats/post/0` returns the site's home page — `/stats/top-posts`
reports it as a pseudo-entry with that id — and the API answers with a
full 200 and complete view history. The home page isn't a post, though,
so three fields come back differently:
post: false (a boolean, not an object and not null)
discussion: null
like_count: null
All three were modelled as required, so the response failed to
deserialize. Comparing a home page payload against a normal post's field
by field, those are the only differences — views, years, averages,
weeks, fields/data and the highest_* trio are identical.
`Option` alone doesn't cover `post`, since `false` is not `null`. Adds a
generic `deserialize_false_as_none` to `wp_serde_helper`, which already
had this quirk covered for `String` and `u64` but not for arbitrary
types. It still errors on a genuinely malformed value rather than
quietly returning `None`.
The new e2e trial also surfaced that three test sites answer any stats
call with `invalid_blog` ("Stats module not enabled"). The existing
trial had been passing on them only because the top-posts lookup failed
first and returned early, so the trial now tolerates that error the way
`stats_region_views_tests` does.
* Trim the stats fixtures
The three fixtures carried 314 lines where 191 exercise the same code
paths. No assertion changed.
- Month maps went from 7-8 entries per year to two; the shape under test
is the map, not its length.
- `homepage.json` came out of the capture with every day and data row
expanded over four lines. Reformatting it to match the other two
fixtures accounts for about a fifth of the saving on its own.
- The unmodelled fields on the post row went from 13 to 3. They exist to
prove serde ignores what we don't model, and three do that as well as
thirteen. Kept `post_content` (a large ignored field), `comment_count`
(the string-typed one we deliberately read from `discussion` instead),
and `filter`.
- `post-no-views.json` had two identical `months: []` years and two
full-length all-zero weeks; one year and two short weeks still cover
both quirks it tests.
Everything asserted survives: the seven-day first week, the four-day
partial week, the `{"isInfinity": true}` third week, all five data rows,
and every scalar.
* Model the per-post stats response against the wp.com source (#1526)
* Reject `true` in `deserialize_false_as_none`, and correct its doc
The helper mapped every boolean to `None`, including `true`. Its siblings
all reject `true` explicitly — `deserialize_false_or_string`,
`deserialize_false_or_string_or_null` and `deserialize_u64_or_none` — so
a value the API is not expected to send was being silently swallowed here
alone. A test case pinned the divergence.
The doc also claimed a missing field yields `None`. Serde does not call
`deserialize_with` for an absent key, so without `#[serde(default)]` a
missing field is a hard error. The new test asserts both halves of that.
Changes:
- Match `Bool(false) | Null` for `None` and error on `Bool(true)`
- Document the `#[serde(default)]` requirement for omitted fields
- Move the `true` case into a new `_errors` test alongside the
malformed-value case
- Add a test covering a missing field with and without `default`
* Model the per-post stats response against the wp.com source
Checking the module against `class.wpcom-json-api-stats-post-views-v1-1-
endpoint.php` and `stats_get_post()` turned up types that overstate what
the API sends, and docs that describe fields it doesn't have.
Address the home page as `StatsPostTarget` rather than `PostId(0)`. Zero
is not a valid `PostId` anywhere else in the crate, and what the API
counts for it is not obvious enough to leave in a doc comment.
`stats_utm` already takes a typed path param this way.
`average`, `averages.overall` and `averages.months` arrive as integers —
PHP casts all three before sending, and FluxC has modelled them as `Int`
for years. Only `change` is a genuine float.
Changes:
- Add `StatsPostTarget`, with `Post { id }` and `HomePage` variants, and
take it as the endpoint's path argument
- Type the three average fields as `u64`
- Type `post_author` as `UserId` and `post_date_gmt` as `WpGmtDateTime`
- Add `post_modified_gmt` and `post_excerpt`, both sent but unmodelled
- Serialize the response back into the `fields`/`data` column table it
parses from, so a serialized response can be read again
- Correct `highest_week_average`, which is the highest single-day count
of recent weeks rather than a weekly average, and `highest_day_average`,
which is a monthly average of daily views
- Document that the home page's figures cover the whole site when the
front page is static, and that `post` is then `null` rather than `false`
- Document that `daily_views` is also empty for a never-viewed target,
whose history the API replaces with one unusable placeholder row, and
cover that row in the column-handling test
- Give `post` a `serde` default, matching the two sibling fields
- Make the fixtures' weeks self-consistent: only the current week is
partial, `average` follows from the days, and `isInfinity` only follows
a zero week
- Name the e2e trials `post::` to match the other stats trials, and
resolve the borrowed post id with `try_from`
* Model `permalink`, and correct the no-views shape against real responses
Captured four responses from a live site to settle two claims the source
alone couldn't. Both were wrong in the module, in opposite directions.
`stats_get_post()` does attach a `permalink`, and it reaches the wire —
it is the 25th key on the post row, after `filter`. The doc comment
asserted the response carried no permalink, and the field was unmodelled,
so callers had no way to reach the post's URL.
A never-viewed post does not get the API's no-history fallback row. Its
daily history is padded from the publication date to today with integer
zeros, so `daily_views` is populated, not empty. What does change is
`years` and `averages`: with no view to anchor on, the API reports every
year from 1970 to the present, each with an empty month map — 57 entries
for a post published last year.
`post-no-views.json` described neither shape. It carried a single 2026
year, which no response can produce.
Changes:
- Add `StatsPostDetails::permalink`, and point `guid` at it
- Note that the post row carries no featured image
- Replace the `daily_views` emptiness note with the padding behaviour
- Document the 1970 year range on `years`
- Rebuild `post-no-views.json` from a real never-viewed response, and
rename its test to match what it covers
- Keep the fallback-row case in the column-handling test, relabelled: the
server can still emit it, but it is not the never-viewed path
* Trim the per-post stats diff to the changes the API requires
Comments and tests that explained serde or documented what the types
used to be, rather than what the endpoint sends.
Changes:
- Drop the `#[serde(default)]` guidance from `deserialize_false_as_none`
and the test asserting serde's missing-field behaviour
- Drop `#[serde(default)]` from the response's `post` field; the endpoint
always sends it
- Restore the fixtures' weeks and the assertions over them, and add only
the three fields the post row gained
- Restore the e2e comments and the `homepage` trial name
- Cut the daily-view case for the API's no-history fallback row, which
is not a shape the endpoint was seen to send
- Cut the notes on how `permalink` is derived and on the absent featured
image
- Use `fmt::Display` rather than a fully qualified path
* Keep the e2e trial names and the post id cast as they were
`post_stats::` says what the trials cover; `post::` names a noun and sits
next to `top_posts::`, where it reads like a posts endpoint rather than a
stats one. The other stats trials drop their `stats_` prefix because what
remains still describes the endpoint, which isn't true here.
Changes:
- Restore the `post_stats::` trial prefix
- Restore the `as i64` cast on the borrowed post id
* Match the crate's error style, and unattach the column comment
Changes:
- Report the rejected `true` with `invalid_value`, as the visitors in
`numeric.rs` do, rather than a custom message
- Use `//` for the note above the two column consts; as a doc comment it
attached to `PERIOD_COLUMN` alone
* Assert the whole response round trips, over every fixture
The test compared only `daily_views`, so it covered the field the column
table flattens into and nothing else. Re-serializing the reparsed value
and comparing covers every field without needing `PartialEq` on the
record.
Runs over all three fixtures. The home page is the case worth having:
its `post` arrives as boolean `false`, becomes `None`, and serializes as
`null`, so it is the only fixture where the round trip changes the wire
shape.
Compares `serde_json::Value` rather than the serialized strings, since
the response holds `HashMap`s and two maps built from the same JSON do
not iterate in the same order.
* Cover the 1970 year range in the no-views test
`years` documents that a target with no views gets an entry for every
year from 1970, and the fixture carries them, but nothing asserted it.
* Let a missing `permalink` be `None` rather than a parse failure
`permalink` is derived per request rather than read from a column, so it
is the field in the post row most likely to move. Requiring it meant an
unexpected shape cost the whole response, for a URL the response is still
useful without.
Changes:
- Type `permalink` as `Option<String>`, read through
`deserialize_false_as_none` with a serde default
* Convert a `PostId` to a target, resolving the home page id
`/stats/top-posts` reports the home page as a pseudo-entry with id 0, so
callers feeding those ids into this endpoint have to know what 0 means.
The conversion puts the rule in one place.
Changes:
- Add `impl From<PostId> for StatsPostTarget`, mapping the home page id
to `HomePage`
- Name the id as `HOME_PAGE_POST_ID` rather than repeating the literal
* Type the post's author as `WpComUserId`
`post_author` on a WordPress.com site carries the account's global id,
which `WpComUserId` names. `UserId` is the wp.org site-scoped id.
`deserialize_i64_or_string_as_t` had no `u64` counterpart, which
`WpComUserId` needs.
Changes:
- Add `deserialize_u64_or_string_as_t` to `wp_serde_helper`
- Type `StatsPostDetails::author_id` as `WpComUserId`
---------
Co-authored-by: Oguz Kocer <oguzkocer@users.noreply.github.com>
Co-authored-by: Oguz Kocer <oguz.kocer@automattic.com>
|
For completeness, here's what Claude had to say about home page stats: How we retrieve home page stats doesn’t need to change. But the caveat is real and worth recording. What the comment is pointing at. The PostId(0) note is about interpretation, not retrieval. From the doc on StatsPostResponse in rs trunk: StatsPostTarget::HomePage requests post 0 … What the view figures cover then depends on how the site’s front page is configured, and the two cases are indistinguishable in the response: a “latest posts” front page — the views recorded against the home page It’s also not a regression. Old stats routes that same row’s id 0 to the same endpoint via PostDetailStore, so both screens have always shown whatever WP.com returns for post 0. Nothing this branch did changed that. On the From for StatsPostTarget they added “so callers working from a list that includes it don’t each repeat the check” — that’s a plain Rust trait impl with no #[uniffi::export], and trait impls don’t cross the FFI boundary. I confirmed the generated Kotlin has no equivalent helper. So my if (postId == HOME_PAGE_POST_ID) HomePage else Post(postId) is the Kotlin counterpart, not a duplicate of something available. |
Description
Follow-up to #1489, targeting
per-post-statsbranch.The per-post stats endpoint has no public documentation, so the response types were worked out from live responses. Comparing them against the endpoint's implementation in
wpcom—class.wpcom-json-api-stats-post-views-v1-1-endpoint.phpandstats_get_post()inwp-content/mu-plugins/stats.php— turned up a field the API sends that wasn't modelled, three types that are wider than what the API can produce, and several doc comments describing behaviour the source contradicts.Two of those could only be settled against the live API, so four responses were captured from a test site: one post with views, two never-viewed posts, and the site's home page.
Changes
Types
StatsPostTargetreplacesPostIdas the endpoint's path argument, withPost { id }andHomePagevariants. The API addresses the home page as post0, which isn't a validPostIdanywhere else in the crate.stats_utmtakes a typed path argument the same way.average,averages.overallandaverages.monthsare nowu64. The endpoint casts all three tointbefore sending them, so a fractional value can't reach the wire — confirmed across all four captured responses.changeis left asf64; it's computed from the uncast average and is the one genuine float in the response.post_authoris nowWpComUserId— on a WordPress.com site the row carries the account's global id, not the site-scoped oneUserIdnames. This needed adeserialize_u64_or_string_as_tinwp_serde_helper, the counterpart to the existingi64variant.post_date_gmtis nowWpGmtDateTime.wp_utc_date_formatalready parses the MySQL datetime format the API uses, so no new deserializer was needed.From<PostId> for StatsPostTargetresolves the home page id, so callers working from a list that includes it —/stats/top-postsreports one — don't each repeat the check.Fields the API sends
permalink—stats_get_post()attaches a computed permalink to the post row, and it reaches the wire as the row's last key. Callers previously had no way to get a post's URL from this response. TypedOption<String>and read throughdeserialize_false_as_none: it's derived per request rather than read from a column, so an unexpected shape shouldn't cost the whole response.post_modified_gmt— the unambiguous counterpart topost_modified, which is in the site's local timezone.post_excerpt.Documentation
PostId(0)doesn't always mean the home page. The endpoint branches on the site'sshow_on_frontsetting: with a "latest posts" front page the figures are the home page's own views, but with a static front page they're the whole site's view history. The response is identical in both cases, so the distinction is documented onStatsPostResponse.highest_week_averageis neither weekly nor an average. It's the highest single-day view count across the last few weeks.highest_day_averageis the highest monthly average of daily views.yearsandaveragesstart at 1970 for a target with no views. With no view to anchor on, the endpoint reports every year from 1970 to the present, each with an emptymonthsmap — 57 entries for a post published last year. Verified on all three never-viewed posts captured.daily_viewsis padded, not sparse. A never-viewed post still gets one zero-count entry per day since publication.Serialization
StatsPostResponsedeserializes from the API'sfields/datacolumn table but derivedSerializeover the flattened shape, so serializing a response produced JSON it could not read back. It now serializes into the column table as well, matching howStatsPostChangehandles its own wire form.wp_serde_helperdeserialize_false_as_nonemapped every boolean toNone, includingtrue. Its siblings —deserialize_false_or_string,deserialize_false_or_string_or_null,deserialize_u64_or_none— all rejecttrueexplicitly, so it now does too. Its doc also listed a missing field as yieldingNone, whichdeserialize_withcannot do without#[serde(default)]; that line is removed.Changelog
CHANGELOG.mdunder## [Unreleased], using the Keep a Changelog categories (Added,Changed,Deprecated,Removed,Fixed,Security). Prefix breaking changes with**BREAKING:**.