Configuration Reference

July 5, 2026 · View on GitHub

proto2type accepts options via buf.gen.yaml (under opt:) or as protoc flags (--proto2type_opt=).

Plugin Options

Command-line options passed to the plugin.

lang

Target language for code generation.

Defaultgo
Valuesgo, rust, python, kotlin, typescript

Note: go, rust, python, and kotlin are currently supported. Other languages are on the roadmap.

Rust-specific behaviour

When lang=rust:

  • Domain types are #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] structs
  • google.protobuf.Timestampchrono::DateTime<Utc>
  • google.protobuf.Durationchrono::Duration
  • Nested messages → Option<Box<T>>
  • repeated / mapVec<T> / HashMap<K, V> with #[serde(default)]
  • Optional fields → Option<T> with #[serde(skip_serializing_if = "Option::is_none")]
  • File suffix: {proto_name}.type.rs (domain), {proto_name}_{backend}.type.rs (storage)

Python-specific behaviour

When lang=python:

  • Models extend pydantic.BaseModel (configurable via base_class)
  • google.protobuf.Timestampdatetime.datetime
  • google.protobuf.Durationdatetime.timedelta
  • google.protobuf.Structdict[str, Any]
  • Nested messages → top-level module class
  • repeated / maplist[T] / dict[K, V]
  • optionalT | None with Field(default=None)
  • Enums → str (Literal) or IntEnum (configurable via enum_style)
  • google.api.field_behavior annotations → Field(...) (REQUIRED) / Field(exclude=True) (OUTPUT_ONLY)
  • buf/validate constraints → Field() kwargs (min_length, max_length, ge, le, gt, lt, pattern)
  • File suffix: {proto_name}_pb2.py

Kotlin-specific behaviour

When lang=kotlin:

  • Domain types are @Serializable data classes using kotlinx.serialization
  • google.protobuf.Timestampkotlinx.datetime.Instant
  • google.protobuf.Durationkotlin.time.Duration
  • Nested messages → nullable T?
  • repeated / mapList<T> / Map<K, V> with defaults emptyList() / emptyMap()
  • Optional fields → T? with default null
  • Enums → @Serializable enum class with @SerialName and fromValue(Int) companion
  • Oneofs → @Serializable sealed class with data class variants
  • File suffix: {proto_name}.type.kt
Python Plugin Options
OptionDefaultDescription
base_classpydantic.BaseModelBase class for generated models
alias_generator(none)Alias generator function (e.g. to_camel for camelCase JSON keys)
enum_styleliteralEnum representation: literal (string Literal) or int (IntEnum)
preset(none)Preset configuration: a2a (sets camelCase aliases + raw enum names)
description(none)Model-level description for generated classes
strip_proto_suffixfalseStrip common proto suffixes (e.g. Request, Response) from class names

backend

Storage backend to generate structs for.

Default(none) — no storage types generated
Valuesfirestore, mongo, sqlite, dynamodb, datastore, spanner

When set, generates a <Name><Backend> struct (e.g. UserFirestore, UserMongo) with backend-specific struct tags and ToProto() / FromProto() converters.

Can be combined with domain=true (default) to generate both domain and storage types, or used with domain=false to generate storage types only.

domain

Controls whether domain types and proto converters are generated.

Defaulttrue
Valuestrue, false

When true, generates a clean native struct (e.g. User) with json:"" tags, time.Time for timestamps, and ToProto() / FromProto() converters.

At least one of domain=true or backend=<name> must be specified.

output_file

Override the default output filename.

Default{proto_name}.type.go (domain), {proto_name}_{backend}.type.go (storage) — Go
{proto_name}.type.rs (domain), {proto_name}_{backend}.type.rs (storage) — Rust
Examplemodels.go

enum_as_string

Controls how proto enums are represented in generated types.

Defaultfalse
When falseEnums are int32
When trueEnums are string (using proto enum value names)

omitempty_default

Controls the default omitempty behavior for optional and zero-value fields.

Defaulttrue
When trueoptional, repeated, map, and message fields get omitempty
When falseOnly fields with explicit (proto2type.field).omitempty = OPTIONAL_BOOL_TRUE get omitempty

validate

Selects the validation strategy for code generation from buf.validate constraints.

Default"" (empty — no validation)
Valuestrue (default per language), validator (Rust), native (Kotlin)

Behavior per language:

LanguageStrategyGenerated Code
Gotrue → protovalidateValidate() method delegating to protovalidate.Validate(d.ToProto())
Rusttrue or validator#[derive(Validate)] + #[validate(...)] attributes (validator crate)
Kotlintrue or nativeExtension functions fun T.validate(): List<String> + fun T.validateOrThrow()
Python(always)Constraints mapped to Pydantic Field() kwargs (no validate flag needed)

Note: Length validation counts characters, not bytes.

Summary Table

OptionDefaultDescription
langgoTarget language
backend(none)Storage backend
domaintrueGenerate domain types + proto converters
output_file(auto)Override output filename
enum_as_stringfalseEnums as string instead of int32
omitempty_defaulttrueDefault omitempty for optional/zero-value fields
validate""Validation strategy from buf.validate constraints

Proto Options

Annotate individual fields and messages in your .proto files to control how proto2type generates code for them.

First, import the options proto:

import "proto2type/options.proto";

(proto2type.field).document_id

Marks a field as the document ID.

Typebool
Defaultfalse

Behavior per backend:

BackendEffect
FirestoreField is excluded from the generated struct — Firestore uses the document path as the ID, not a struct field
MongoDBField tag becomes bson:"_id" — maps to MongoDB's _id document key
(domain)No effect — field is included normally with json:"" tag
message User {
  string id = 1 [(proto2type.field).document_id = true];
}

(proto2type.field).server_timestamp

Marks a timestamp field as server-managed.

Typebool
Defaultfalse

Behavior per backend:

BackendEffect
FirestoreTag becomes firestore:"field_name,serverTimestamp" — Firestore sets the timestamp on write
MongoDBNo special behavior (use MongoDB server-side $currentDate in your queries)
(domain)No effect — field is a normal time.Time
message User {
  google.protobuf.Timestamp updated_at = 10 [(proto2type.field).server_timestamp = true];
}

(proto2type.field).skip

Excludes the field from all generated types (domain, storage, and converters).

Typebool
Defaultfalse

Use this for internal-only fields that should not appear in application-layer code.

message User {
  string internal_trace_id = 99 [(proto2type.field).skip = true];
}

(proto2type.field).omitempty

Force-override the omitempty behavior for a specific field.

TypeOptionalBool
DefaultOPTIONAL_BOOL_UNSPECIFIED (uses omitempty_default plugin option)
ValuesOPTIONAL_BOOL_TRUE, OPTIONAL_BOOL_FALSE
message User {
  // Always include category in JSON/storage even when empty
  string category = 7 [(proto2type.field).omitempty = OPTIONAL_BOOL_FALSE];
  // Always omit roles when empty
  repeated string roles = 8 [(proto2type.field).omitempty = OPTIONAL_BOOL_TRUE];
}

(proto2type.field).inline

Flattens a nested message's fields into the parent struct.

Typebool
Defaultfalse

Behavior per backend:

BackendEffect
MongoDBAdds bson:",inline" to the struct tag — embeds nested document fields at the parent level
FirestoreNo direct equivalent — nested message is stored as a sub-map
(domain)No effect — field remains a pointer to the nested struct
message User {
  Address address = 5 [(proto2type.field).inline = true];
}

Generated MongoDB struct:

type UserMongo struct {
    // ...
    Address *AddressMongo `bson:",inline"`
}

(proto2type.field).name

Overrides the storage field name used in struct tags.

Typestring
Default(proto field name in snake_case)

This affects firestore:"", bson:"", and json:"" tag values.

message User {
  string display_name = 3 [(proto2type.field).name = "name"];
}

Generated:

DisplayName string `json:"name"`          // domain
DisplayName string `firestore:"name"`     // firestore
DisplayName string `bson:"name"`          // mongo

(proto2type.message).skip

Skips generating types for the entire message.

Typebool
Defaultfalse

Use this for messages that are only used as proto wire types and should not have generated domain or storage types.

message InternalRpcRequest {
  option (proto2type.message).skip = true;
  string trace_id = 1;
}

Backend Reference

Firestore

FeatureSupport
Struct tagfirestore:""
Document IDdocument_id=true → field excluded from struct (Firestore uses doc path)
Server timestampsserver_timestamp=truefirestore:"field,serverTimestamp"
Omitemptyfirestore:"field,omitempty"
Struct suffixFirestore (e.g. UserFirestore)
File suffix_firestore.type.go

Example generated output:

type UserFirestore struct {
    Email       string    `firestore:"email"`
    DisplayName string    `firestore:"display_name"`
    Active      bool      `firestore:"active"`
    CreatedAt   time.Time `firestore:"created_at,serverTimestamp"`
    UpdatedAt   time.Time `firestore:"updated_at,serverTimestamp"`
}

func (d *UserFirestore) ToProto() *userpb.User { ... }
func (d *UserFirestore) FromProto(pb *userpb.User) { ... }

MongoDB

FeatureSupport
Struct tagbson:""
Document IDdocument_id=truebson:"_id"
Inline embeddinginline=truebson:",inline"
Omitemptybson:"field,omitempty"
Struct suffixMongo (e.g. UserMongo)
File suffix_mongo.type.go

Example generated output:

type UserMongo struct {
    ID          string        `bson:"_id"`
    Email       string        `bson:"email"`
    DisplayName string        `bson:"display_name"`
    Active      bool          `bson:"active"`
    Address     *AddressMongo `bson:",inline"`
    CreatedAt   time.Time     `bson:"created_at,omitempty"`
    UpdatedAt   time.Time     `bson:"updated_at,omitempty"`
}

func (d *UserMongo) ToProto() *userpb.User { ... }
func (d *UserMongo) FromProto(pb *userpb.User) { ... }

SQLite (Rust)

FeatureSupport
LanguageRust (lang=rust)
Struct suffixRow (e.g. UserRow)
File suffix_sqlite.type.rs
Timestampsi64 epoch milliseconds
Durationsi64 milliseconds
Nested messagesString (JSON-serialised via serde_json)
repeated / mapString (JSON-serialised via serde_json)
Row constructionfrom_row(&rusqlite::Row) for named column access
Domain convertersto_domain() / from_domain() methods

Example generated output:

// Code generated by proto2type. DO NOT EDIT.
// backend: sqlite

use super::*;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserRow {
    pub id: String,
    pub email: String,
    pub display_name: String,
    pub active: bool,
    pub created_at: i64,        // epoch milliseconds
    pub session_timeout: i64,   // duration in milliseconds
    pub address: String,        // nested message as JSON
    pub roles: String,          // repeated field as JSON
}

impl UserRow {
    pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> { ... }
    pub fn to_domain(&self) -> Result<User, serde_json::Error> { ... }
    pub fn from_domain(d: &User) -> Result<Self, serde_json::Error> { ... }
}

Example: Full buf.gen.yaml

Generate domain types, Firestore storage, and MongoDB storage from the same proto:

# buf.gen.yaml
version: v2
plugins:
  # Domain types (json tags, time.Time, converters)
  - local: protoc-gen-proto2type
    out: gen/go
    opt:
      - lang=go

  # Firestore storage types
  - local: protoc-gen-proto2type
    out: gen/go
    opt:
      - lang=go
      - domain=false
      - backend=firestore

  # MongoDB storage types
  - local: protoc-gen-proto2type
    out: gen/go
    opt:
      - lang=go
      - domain=false
      - backend=mongo

This produces three files per proto:

  • user.type.go — domain types with json:"" tags
  • user_firestore.type.go — Firestore structs with firestore:"" tags
  • user_mongo.type.go — MongoDB structs with bson:"" tags

All three include ToProto() and FromProto() converters.

Security Considerations

JSON Deserialization Trust Model

The SQLite backend serializes nested messages, repeated fields, and maps as JSON strings in TEXT columns. The generated to_domain() and into_domain() methods deserialize these values using serde_json::from_str().

Trust Assumption: The SQLite database is treated as a trusted data source. If the database is writable by untrusted parties, consider:

  • Adding input size validation before deserialization
  • Using serde_json::from_reader with a bounded reader
  • Validating JSON depth before processing

serde_json applies a default recursion limit of 128 levels, which mitigates deep-nesting attacks.

Sensitive Data

Generated structs derive Debug, Serialize, and Deserialize for all fields. This means:

  • Debug formatting will include all field values, including PII
  • Serialization will include all fields

For fields containing sensitive data, consider:

  • Adding #[serde(skip)] manually to generated files (not recommended — will be overwritten)
  • Filing an issue to add a (proto2type.field).sensitive option
  • Using a wrapper type that implements custom Debug

Supply Chain

Generated Rust code depends on these crates:

CrateMin VersionNotes
serde1.0Widely audited
serde_json1.0Widely audited
chrono0.4.20+Pin to avoid RUSTSEC-2020-0159
rusqlite0.35C FFI to SQLite
validator0.18Used when validate=true for #[derive(Validate)]
lazy_static1.4Used by generated pattern/UUID validation code
regex1.0Used by generated pattern validation code

Run cargo audit regularly on projects using generated code.