Skip to content

buffa-build: option to generate idiomatic snake_case Rust field names from camelCase proto fields (prost parity) #256

Description

@jlucaso1

Migrating a codebase from prost to buffa and hit a snag around field naming. Writing it up since the fix looks small and it's a pretty common interop need.

What I need

Plenty of .proto files in the wild use camelCase field names — proto3-JSON style, or protos that are synced/generated from an external source and kept verbatim. With prost these generate idiomatic snake_case Rust fields automatically (webMessageInfoweb_message_info) while the wire/JSON name stays the original. buffa instead emits the Rust ident verbatim from the proto field name, so webMessageInfo in the proto becomes msg.webMessageInfo in Rust — non-idiomatic, and it breaks any code written against the prost-style field names.

So today the only options are:

  1. Hand-rename every field in the .proto to snake_case. That doesn't survive regenerating the proto from its upstream source, and it's a large diff to keep in sync.
  2. Post-process the FileDescriptorSet yourself in build.rs (decode → to_snake_case every field.name → re-encode → feed the rewritten descriptor to buffa via descriptor_set). Works, but it's boilerplate every buffa user with a camelCase proto ends up reinventing.

How prost does it

prost-build does this conversion for you. Its codegen runs proto field names through heck's ToSnakeCase to produce the Rust ident and keeps the original name as the proto/JSON name, so a camelCase proto "just works" and the generated API is idiomatic Rust:

message WebMessageInfo {
  string remoteJid = 1;
  uint64 messageTimestamp = 2;
}
// prost output
pub struct WebMessageInfo {
    pub remote_jid: String,
    pub message_timestamp: u64,
}

buffa's make_field_ident only escapes Rust keywords; it doesn't case-convert, so the same proto yields remoteJid / messageTimestamp fields.

The key point: this is purely a Rust-ident concern. The wire format keys on field numbers, and the proto/JSON name can stay the original camelCase — so the conversion is fully wire- and JSON-compatible.

Proposal

Add an opt-in codegen option, mirroring prost's default behavior. Something like:

buffa_build::Config::new()
    .idiomatic_field_names(true)   // or field_case(FieldCase::Snake)
    // ...

When enabled, make_field_ident would apply to_snake_case to the proto field name (and oneof field names), while proto_name() / the serde rename / reflection keep the original name for wire + JSON parity. Default off, so existing output is unchanged.

A couple of finer points worth deciding:

  • Global only vs. per-path (_in variant). Global is enough for my case, but per-path would match the rest of the API.
  • json_name: keep emitting the descriptor's json_name (camelCase) so proto3-JSON round-trips unchanged.

This feels symmetric with the existing idiomatic_enum_aliases option — same "make the generated Rust nicer without touching the wire" spirit. Happy to send a PR if the direction sounds good; it looks like a small change localized to the ident generation (make_field_ident + a CodeGenConfig flag).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions