prost-reflect 0.16.5 doesn’t just reject a Protobuf Editions file — it panics while formatting the error message that was supposed to explain why.
Today we released OxiProto 0.1.5 — the COOLJAPAN Pure-Rust Protocol Buffers stack. This release brings Protobuf Editions (edition = "2023";) support the rest of the way: full spec-compliant feature resolution, decode-time enforcement, and — because the facade library everyone’s reflection path runs through can’t handle Editions files at all — a rewrite step that downlevels an Editions file into the proto2 equivalent it can.
No C++. No protoc binary on PATH. OxiProto’s native .proto parser and codegen stay Pure Rust; this release makes them correct for a syntax that Protobuf’s own reference tooling shipped over a year ago and most of the Rust ecosystem still can’t read.
Why OxiProto 0.1.5 is a game changer
Editions replaced proto2/proto3’s fixed syntax choice with six independently resolvable features — but getting from a parsed .proto file to something usable turned out to require solving three separate problems:
- Feature resolution itself.
field_presence,enum_type,repeated_field_encoding,utf8_validation,message_encoding, andjson_formateach resolve through an inheritance chain — file → message → nested message → oneof → field, and enum → enum value — against per-edition baselines. Getting this wrong doesn’t fail loudly; it silently changes wire bytes. prost-reflectrecognizes exactly twosyntaxvalues. Anything else — including"editions"— is refused, and in 0.16.5 the refusal itself panics while formatting the error. Every consumer ofoxiproto_reflect::pool_from_fds(the re-exportedDynamicMessage,oxiproto-json, the CLI’sencode/decode) was unusable for an Editions schema.- Enforcement, not just parsing.
utf8_validation = NONEandenum_type = CLOSEDare proto2-era behaviors Editions makes explicit and selectable — resolving the feature is only half the job if decode doesn’t actually act on it.
OxiProto 0.1.5 solves all three:
- A new
oxiproto_build::parser::featuresmodule resolves the full six-feature set through the correct inheritance chain, rejects removed constructs (optional/required/groupin an Editions file) and misapplied features (presence on a repeated field, packing on a non-repeated one) with typed errors, and materializes the resolved features back intodescriptor.proto-compatible form (sinceprost-types0.14 doesn’t model Editions at all) asuninterpreted_optionentries that survive theFileDescriptorSetboundary. - A new
oxiproto_reflect::editionsmodule (downlevel_editions) rewrites an Editions file into its proto2 equivalent — the syntax baseline that actually preservesLABEL_REQUIRED,TYPE_GROUP, and explicit presence — with an explicitFieldOptions.packedwritten wherever proto2’s default (expanded) would otherwise diverge from Editions’ default (packed). Both reflection pool constructors apply it automatically; non-Editions files pass through byte-identically. utf8_validationandenum_typeare now enforced at decode time everywhere, not just parsed and ignored —NONE-validation strings land in a new typedValue::UnvalidatedStringthat round-trips byte-identically, and closed-enum unrecognized values move to the unknown-field set exactly as proto2 implementations have always done.
Technical Deep Dive: three bugs the Editions work surfaced elsewhere
- Generated code packed proto2 repeated scalars, diverging from
protocon the wire.oxiproto-codegendefaulted every unpacked-by-omission repeated scalar to packed, butprotocemits the expanded form for proto2. Fine for decoding (both forms are accepted either way) but wrong for anything doing byte-comparison, signing, or deterministic serialization against a realprotoc-generated peer. The generator now classifies the file’s syntax and applies the correct per-syntax default. - Generated packed-repeated decode read from the wrong buffer. The packed element loop tested the nested packed buffer for emptiness but read from the enclosing message’s buffer — silently consuming the next fields on the wire or throwing
UnexpectedEof. The identical bug hit map-entry key/value decoding. Found by the new proto2 packing tests, now fixed with the buffer name threaded explicitly. - Nested types referenced an undefined name in flat codegen layout. A field referring to
.pkg.Outer.Innergenerated code referencing the bareInnerinstead of the flattenedOuter_Innercodegen actually emits — code that didn’t compile whenever a schema had a nested message or enum field. This is unavoidable to trip forgroupfields, which always synthesize a nested message.
Both new nesting-depth DoS bounds — a 100-level cap on the text-format parser/encoder and a matching cap on the .proto source parser — close the same unbounded-recursion class the 0.1.4 wire-decode fixes addressed, now covering the two paths that were still open: DynamicMessage::from_text/to_text, and every CLI subcommand that reads a user-supplied .proto file.
Getting Started
[dependencies]
oxiproto = "0.1.6"
[build-dependencies]
oxiproto-build = "0.1.6"
fn main() {
oxiproto_build::compile_protos(&["proto/my_service.proto"], &["proto/"]).unwrap();
}
Or reflect over an Editions file directly — the downlevel rewrite happens automatically:
use oxiproto_reflect::pool_from_fds_bytes;
// Works even though the source .proto declares `edition = "2023";` --
// pool_from_fds_bytes downlevels it to proto2 internally before handing
// it to the prost-reflect facade, which cannot parse "editions" at all.
let pool = pool_from_fds_bytes(&file_descriptor_set_bytes)?;
What’s New in 0.1.5
- Added: Editions 2023 support end to end (feature resolution, enforcement,
prost-reflectdownlevel facade);features.utf8_validationandfeatures.enum_typenow enforced at decode time for all syntaxes, not just Editions files; native proto2groupfield support inoxiproto-reflect(wire, JSON, text-format);rustfmt.toml/clippy.tomlat the workspace root. - Security: message-nesting depth bounded in both the text-format parser/encoder and the
.protosource parser (100-level cap, typed errors instead of stack overflow). - Changed: presence-aware serialization — a field with presence (proto2
optional, a oneof member, a message field, or an Editions field withEXPLICITpresence) explicitly set to its type default is now emitted on the wire, in JSON, and in text, instead of being silently dropped (proto2’s “set to 0” vs “unset” distinction, restored);optional/required/groupare now hard errors in an Editions file instead of a proto3 approximation. - Fixed: the proto2 packing-default divergence, the wrong-buffer packed-decode bug, and the flat-codegen nested-type naming bug above; 7
.expect()panic sites removed from the.protolexer/parser hot path, including one that could panic on a bad UTF-8 char boundary before the “cannot fail” assertion was even reached.
Tips
- If you’re on an Editions schema and reflection previously failed or panicked, just upgrade —
pool_from_fds/pool_from_fds_byteshandle the downlevel transparently; no call-site change needed. - Watch for the one intentional 0.x breaking change:
native::ValuegainedUnvalidatedString(Vec<u8>). It’s additive but the enum isn’t#[non_exhaustive], so an exhaustivematchonnative::Valueneeds a new arm — it’s only ever produced by astringfield whose resolvedfeatures.utf8_validationisNONE. - If you maintain proto2 schemas and compare serialized bytes against
protocoutput (signing, determinism, golden fixtures), this release’s packing-default fix changes generated-code output for repeated scalar fields without an explicit[packed = ...]— re-generate and re-diff. - Proto2 fixtures that explicitly set a field to its zero value may see new bytes on the wire — presence-aware serialization now emits those fields instead of dropping them, which is the correct proto2 behavior but a behavior change if you had fixtures built against the old (incorrect) drop-on-default logic.
optional/required/groupin anedition = "2023"file are now compile errors, not silent proto3 approximations — if you were relying on the old synthetic-oneof behavior foroptionalin an Editions file, express presence viafeatures.field_presenceinstead.
This is the foundation
A protobuf stack that gets Editions right — resolution, enforcement, and interop with the reflection facade everyone actually uses — matters for anything doing schema evolution across proto2, proto3, and Editions in the same fleet. OxiRPC pins oxiproto/oxiproto-core/oxiproto-build/oxiproto-reflect for its RPC wire layer, and OxiRS pins oxiproto-build for protobuf codegen without a protoc dependency.
Repository: https://github.com/cool-japan/oxiproto
Star the repo if “the reflection library panics on the word ‘editions’” is exactly the kind of gap you’d rather have closed before you hit it in production.
The era of Protobuf Editions being a syntax you could declare but not actually use is over. Pure Rust Protocol Buffers that are fast, safe, and sovereign — is here.
— KitaSan at COOLJAPAN OÜ August 6, 2026