RFA-479 · Case file with fixtures · Case 451 of 694 · Compiler evidence
Private Rust Struct Fields Require a Public Constructor Boundary
Direct struct literals require access to every named field they initialise. Keep invariant-bearing fields private and provide a checked constructor or builder; make fields public only when arbitrary caller values are supported API.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Direct struct literals name representation fields individually, and this caller lacks access to one field required for construction.
- First discriminating check
- Use or add a checked public constructor or builder, making the field public only if arbitrary caller mutation and representation coupling are intended API.
I made Config public and exposed its name field, but kept secret private. Constructing it with model::Config { name: "api", secret: 7 } outside the module produced E0451.
The failing fixture shows that visibility applies to fields independently for named structs. Access to the type does not grant access to all of its representation.
A struct literal needs field-level access
Direct construction names and supplies the fields. Rust verifies that the current module may access each one.
The E0451 explanation offers making fields public or providing a construction function. Those options encode different API promises.
If even one required field is private, downstream code cannot use a complete literal or bypass it with struct update syntax.
Constructors protect invariants
The repaired fixture adds Config::new inside the defining module. That function can access both fields and returns a valid instance.
In production code, the constructor may validate ranges, normalise input, derive internal state, or return Result<Self, Error>. Callers depend on the construction contract instead of the representation layout.
I keep the constructor small enough that its invariants are visible and testable.
Public fields permit arbitrary direct states
Making secret public would let any caller set it, replace it through destructuring or update syntax, and couple code to its exact type. This may be appropriate for plain data-transfer records with intentionally transparent representation.
For stateful domain objects, caches, security tokens, and validated identifiers, private fields preserve freedom to change representation and maintain rules.
I do not add pub only because a test wants convenient literal syntax.
Builders help with many optional inputs
A constructor with many parameters becomes hard to read, especially when several share a type. A builder can expose named choices, defaults, and final validation while fields remain private.
The builder itself should distinguish missing required data from defaultable data. I avoid builders that defer every mistake to a late panic.
For a small two-field value, one direct constructor is usually simpler.
Getters do not need to reveal storage
The repaired type exposes has_secret() rather than the raw private integer. A getter may return a borrow, derived property, or copied value according to the stable information callers need.
I do not automatically write getters and setters for every field. That recreates public representation through methods while adding ceremony.
Operations should preserve the abstraction and invariants the private field was meant to protect.
Tests should respect the intended boundary
Unit tests nested inside the defining module can access private fields; integration tests compile as downstream crates and cannot. E0451 after moving a test may reveal that it relied on internal construction.
I use public constructors in integration tests to verify the real consumer experience. For rare invalid-state tests, a module-local test helper can remain near the implementation.
The visibility Reference explains access through module ancestry.
Destructuring has the same privacy concern
Downstream code cannot pattern-match private fields directly. Public methods or a public view enum can expose supported observations without freezing internal layout.
The struct-expression Reference covers construction and update syntax, but visibility still governs whether each field may be named.
Serialization derives may access fields inside the defining crate while controlling external wire shape separately; Rust visibility and data-format exposure are not the same boundary.
My E0451 checklist
When a type crosses FFI, private Rust fields do not by themselves make its layout safe or stable. repr(C), ownership rules, and constructor validation are separate concerns. I avoid letting foreign code fabricate an invariant-bearing Rust value through raw layout merely because direct Rust literals are blocked. Opaque handles and dedicated boundary functions often preserve the contract better. The E0451 repair is about source privacy; the ABI design still needs its own evidence and tests.
- Which field is private at the construction site?
- Is direct arbitrary construction intended public API?
- What invariant or evolution freedom does privacy protect?
- Should a constructor validate and return
Result? - Would a builder clarify many optional arguments?
- What observation should methods expose instead of raw storage?
- Is an integration test relying on private representation?
- Does serialization expose data despite Rust field privacy?
The core principle is that a public type can expose behaviour without exposing construction of every representation state. I keep invariant-bearing fields private and offer the smallest public constructor and observation surface that callers can use safely.