buffa-yaml: YAML serialization extension crate
Maintainers usually reply within 2 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 48/100
- Issue type
- Feature
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- rust
- Domain
- backend-api-design
Research direction
Start by reviewing the workspace layout and the proposed buffa-yaml/Cargo.toml dependencies, then read buffa's JSON helpers and generated serde implementations. Use src/lib.rs, encode.rs, decode.rs, and error.rs as the planned entry points. Done means the five wrapper functions, Location-preserving Error, documented behavioral deltas, and the listed buffa-test WKT, scalar, map, and fixture round-trips are covered.
Written by the indexing model from the issue text.
Description
Tracking issue for adding YAML support to buffa, equivalent to what bufbuild/protoyaml-go provides for Go: marshal and unmarshal protobuf messages as YAML, fully compatible with the protobuf-canonical JSON mapping. Useful for hand-authored config files, test fixtures, and anywhere YAML is more readable than JSON.
How protoyaml-go is built (and why buffa is structurally different)
protoyaml-go is asymmetric: encode delegates to protojson (proto → JSON bytes → re-emit as YAML), decode is a hand-rolled *yaml.Node tree walker driven by protoreflect. The decode side is hand-rolled for two reasons: rich diagnostics (file:line:col + source line + ^ pointer), and lenience extensions beyond protojson (hex/octal/binary int literals, byte-size suffixes like 1Ki, Go-style durations like 1h30m, field-number addressing).
buffa's situation is different. buffa's JSON support is serde-derive-based — the generated Serialize/Deserialize impls and buffa::json_helpers with-modules encode protobuf-JSON semantics through serde's carrier-agnostic data model. They never call format-specific APIs except using serde_json::Value as an opaque scratch type (which any carrier can populate, since Value: Deserialize). This means the equivalent of protoyaml-go's encode and decode paths already exist if the carrier is swapped from serde_json to a YAML serde crate:
let yaml = serde_norway::to_string(&msg)?; // shared Serialize impl
let msg: MyMessage = serde_norway::from_str(yaml)?; // shared Deserialize impl
What you would not get from this is the diagnostic snippets and the lenience extensions. Those are explicitly out of scope for Phase 1.
Design: buffa-yaml extension crate
A new published workspace member rather than a feature flag on buffa core:
- Decouple YAML-crate churn from buffa's versioning. The carrier crate ecosystem is unstable (
serde_yamldeprecated 2024-03,serde_ymlis RUSTSEC-2025-0068 unsound + unmaintained,serde_norway/serde_yaml_ng/serde-saphyrare the survivors). If the carrier needs to change, that's abuffa-yaml0.x.y bump, not abuffasemver event. - Curate the supply chain. Pinning a vetted carrier in a published crate steers users away from the CVE-bearing fork.
- Keep
buffacore's dependency surface minimal. YAML carriers arestd-only; buffa core isno_std-capable. - Room to grow. A future lenience layer (Phase 2) needs a home.
- Precedent. The
jsonandtextfeatures inbuffacore gate runtime infrastructure. YAML doesn't need new runtime infrastructure — it reuses the JSON serde impls. The thing it adds is a carrier dependency and convenience wrappers.
Carrier choice: serde_norway
Empirically validated by round-tripping buffa-test's generated types (json_types, edge_cases non-string maps, ext_json extension-range messages) plus WKTs through both candidates:
serde_norway(0.9.42, maintained fork ofserde_yaml, wrapsunsafe-libyaml-norway): all round-trips clean, zerobuffachanges required. Inherits dtolnay's restricted scalar resolution, soname: noarrives as a string (no Norway problem). Float specials (Infinity,.nan) are delivered asvisit_f64, which buffa'sfloat/doublehelpers already accept.!!binaryis passed through as the raw text; buffa's single base64 decode produces the right answer.serde-saphyr(0.0.26, pure Rust, span diagnostics,forbid(unsafe_code), anti-DoS budgets): three conflicts found, all addressable but each needs work — (a) it canonicalizes float specials to.nan/.infstrings indeserialize_any(designed soserde_json::Valueround-trips them), which buffa'svisit_strrejects; (b) it pre-decodes!!binaryso buffa'svisit_strdecodes again (double-decode), needsignore_binary_tag_for_string: true; (c) defaults to YAML 1.1 lenient bools (Norway problem), needsstrict_booleans: true. Also0.0.xwith theOptionsAPI mid-deprecation.
serde_norway is the safe Phase 1 default. serde-saphyr is the more interesting long-term carrier — its diagnostics and security posture align with what makes protoyaml-go valuable — but switching to it would need (a) a gated JsonParseOptions::accept_yaml_float_literals option in buffa core (so JSON parsing stays strictly conformant; JsonParseOptions is already #[non_exhaustive]), and (b) the Options API to settle. Track as a future revisit.
A note on #[serde(flatten)]: this was a concern (saphyr documents flatten limitations), but it turns out to be a non-issue. buffa's codegen never uses serde's flatten machinery on the deserialize side — every message that has a flattened oneof or extension wrapper also gets a hand-written visit_map Deserialize impl. The derived #[derive(Serialize)] does use #[serde(flatten)], but serialize-side flatten is just FlatMapSerializer collecting keys into the parent map, which every carrier supports. Confirmed in the spike.
API surface (Phase 1)
Free functions mirroring serde_json / serde_norway conventions, with a Message bound for discoverability and future-proofing:
pub fn to_string<M: buffa::Message + serde::Serialize>(msg: &M) -> Result<String, Error>;
pub fn to_writer<W: io::Write, M: buffa::Message + serde::Serialize>(w: W, msg: &M) -> Result<(), Error>;
pub fn from_str<M: buffa::Message + serde::de::DeserializeOwned>(s: &str) -> Result<M, Error>;
pub fn from_slice<M: buffa::Message + serde::de::DeserializeOwned>(b: &[u8]) -> Result<M, Error>;
pub fn from_reader<R: io::Read, M: buffa::Message + serde::de::DeserializeOwned>(r: R) -> Result<M, Error>;
Error wraps the carrier's error type and exposes its Location (line/col) so callers can render diagnostics.
Layout:
buffa-yaml/
Cargo.toml # buffa (default-features=false, features=["json","std"]), serde, serde_norway
src/
lib.rs # docs + re-exports
encode.rs # to_string, to_writer
decode.rs # from_str, from_slice, from_reader
error.rs # Error wrapper preserving Location
Documented behavioral deltas vs protoyaml-go
buffa-yaml Phase 1 targets "protobuf-JSON semantics on a YAML carrier," not full protoyaml-go parity. Differences to document up front:
| Capability | protoyaml-go | buffa-yaml Phase 1 |
|---|---|---|
| protojson-equivalent encode/decode | ✅ | ✅ |
| camelCase + snake_case field names | ✅ | ✅ (#[serde(rename, alias)]) |
Hex/octal int literals (0x1F) |
✅ | ✅ (carrier scalar resolution) |
Field number as YAML key (13: true) |
✅ | ❌ |
Reject YAML 1.1 bool aliases (True/TRUE) |
✅ (only true/false) |
❌ — carrier-defined, accepts case variants |
Byte-size suffixes (1Ki, 2Gi) |
✅ | ❌ |
Go-style duration (1h30m) |
✅ | ❌ — protojson "1.5s" only |
| Line/column in errors | ✅ | ✅ (Location) |
^ pointer + snippet rendering |
✅ | ❌ |
Validation hooks (protovalidate) |
✅ | n/a |
| Encode from view types | n/a | ❌ — blocked on #83 |
The "free wins" from the carrier (hex literals, RFC3339, multi-form aliases, .inf-as-float) get us partway to protoyaml-go's lenience without writing any code.
Test plan
A #[cfg(test)] module driving buffa-test's generated types through round-trips:
- Every WKT:
Timestamp,Duration,Any,Struct/Value/ListValue,FieldMask,Empty, all wrappers. int64as quoted string round-trip (>2^53 precision check),doubleNaN/Inf as string token, base64 bytes, multi-line strings.- Oneof field naming (serialize-side
#[serde(flatten)]). - Extension-range messages (serialize-side flatten of the ext wrapper).
- Maps with string/int/bool keys (carrier coerces YAML int keys to string, buffa's int-keyed map deserializer parses them — confirmed in spike).
- Repeated/map enum with unknown-value filtering (the
with_json_parse_optionsthread-local path — works carrier-agnostically since it routes throughserde_json::Value). - A hand-written
.yamlfixture exercising YAML-specific scalar resolution surprises (yes/0x1F/~/<<:merge key/!!binary) and asserting the actual carrier behavior, to lock it in as a documented baseline. This is the test most likely to catch a carrier-version regression.
Out of scope (potential follow-ups)
- Lenience layer (byte-size suffixes, Go durations, field-number addressing, snippet diagnostics). These need either a YAML→
serde_json::Valuebridge with a normalization pass, or runtime reflection (#9) for type-directed parsing. Realistically a 600–1000 LOC effort. File only if there's actual demand for1Ki-style literals. accept_yaml_float_literalsparse option inbuffacore, gated behindJsonParseOptions. Sketch is ready; only needed if the carrier changes to one that string-canonicalizes float specials (seeserde-saphyrnotes above).- musli rearchitecture. A unified mode-parameterized text-format framework would solve the lenience problem at the type level instead of with ambient state, but it's a buffa 2.0-scale project. Tracked separately in #100.
- View encoding. Blocked on #83 (
Viewtypes lackserde::Serialize).
References
bufbuild/protoyaml-go— the prior art; encode delegates to protojson, decode is a hand-rolled*yaml.Nodewalker for diagnostics + lenience.serde_norway— chosen carrier.serde-saphyr— future carrier candidate; rich diagnostics,forbid(unsafe_code), anti-DoS budgets, but0.0.xand needs theJsonParseOptionsoption to round-trip float specials.- RUSTSEC-2025-0068 — why
serde_ymlmust not be the carrier. - #100 — musli as buffa's text-format framework (long-term alternative architecture).
- #83 — View types lack
serde::Serialize(blocks view encoding).
- Dominant language
- Rust
- Stars
- 900
- Forks
- 95
- Avg merge
- 5d 10h
- Merged PRs (30d)
- 57
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from anthropics/buffa
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
anthropics/buffa#487 ·
Maintainers usually reply within 2 days
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
anthropics/buffa#490 · 2 reactions ·
Maintainers usually reply within 2 days
-
enhancement
Difficulty 4/5 3-5 days Newbie friendliness 55/100
anthropics/buffa#482 · 1 comment ·
Maintainers usually reply within 2 days
-
enhancement
Difficulty 5/5 Over a week Newbie friendliness 35/100
anthropics/buffa#463 ·
Maintainers usually reply within 2 days
-
Difficulty 3/5 1-2 days Newbie friendliness 72/100
anthropics/buffa#450 ·
Maintainers usually reply within 2 days
All issues in anthropics/buffa
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
stellar/stellar-cli#2773 ·
Maintainers usually reply within 2 days
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
voidzero-dev/oxc-angular-compiler#511 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 1-3 hours Newbie friendliness 86/100
yantrikos/yantrik-os#539 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
documentation station:mac ui-dashboard
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
rolter-ai/rolter#2490 · 1 comment ·
Maintainers usually reply within 1 day