RFA-580 · Case file with fixtures · Case 552 of 694 · Compiler evidence
Rust Field Access Must Match the Resolved Struct Definition
Named-field access follows the exact resolved type, not a remembered schema. Verify type identity, feature configuration, and API evolution before editing.
- 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
- Code relied on a remembered or external schema rather than the exact type definition selected under the active imports, features, and generator version.
- First discriminating check
- Navigate to the resolved type, verify its active fields and domain meaning, then use an existing member or evolve the owned schema deliberately.
Named field access is checked against the concrete type Rust resolved for the expression. The failing fixture creates Metrics { requests: 12 } and later reads metrics.errors. Since Metrics has no field with that name, rustc reports E0609 and lists the available field.
The diagnostic is about schema, not runtime contents
A Rust struct is not a map whose keys can appear as data changes. Its field set is part of the type definition and is known during compilation. No runtime branch can make an undeclared field exist.
The official E0609 explanation recommends checking spelling and whether the field exists. I extend that first check to type identity. In a large workspace, two modules may export types called Metrics, generated bindings may have changed, or inference may have selected a wrapper rather than the record I expected.
Hover information helps, but I navigate to the resolved definition and inspect the active source. A similarly named struct from another crate is not evidence about this expression.
The smallest repair uses the declared field
The repaired fixture reads requests and asserts its value. This is correct because that was the intended metric in the small example.
In production, changing errors to the first suggested field without understanding the domain can be dangerous. Requests and errors carry different meaning. If the caller truly needs an error count, the data model may need to add it, derive it from labelled counters, or ask another component. A spelling fix and a missing capability are different repairs.
I check the declaration's history. If a dependency renamed a public field, I read its migration notes and look for a method that replaced representation access. If my own type changed, I update constructors, serializers, snapshots, and consumers together.
Field access can cross automatic dereferencing
Rust's field expression rules can automatically dereference a receiver while searching for a field. This makes references and smart-pointer-like values convenient, but it can obscure which underlying type owns the member.
I write the receiver type down when the error is unexpected. If a wrapper intentionally hides an inner representation, exposing a method such as error_count() may be better than adding public fields or manually reaching through layers.
Tuple fields use numeric positions such as .0; named structs use declared identifiers. These are separate structural contracts. Converting a record into a tuple just to obtain positional access usually reduces clarity.
Features and generated schemas can change the active shape
A field may exist only under cfg. Code that compiles locally can fail for another target or feature set if it accesses that field unconditionally. I inspect attributes on both the field and its surrounding type, then build the supported matrix.
Generated API clients and protocol bindings create another common source. The external schema might contain errors, while generated Rust uses a getter, sanitised identifier, nested message, or renamed field. I treat the generated API as the actual Rust contract and translate external concepts at one boundary.
I do not hand-edit generated output. The change will disappear at regeneration and may diverge from the schema. I update the generator input, generator version, or adapter layer and add a reproducible generation check.
Public fields create compatibility obligations
Making a field public fixes privacy errors, but E0609 means there is no field at all. Adding one to a public struct may break downstream exhaustive construction and pattern matching depending on the API. It also commits to a representation name and type.
For evolving libraries I often keep fields private and expose behaviour through methods. Builders and constructors validate invariants, while accessors allow representation to change. Data-transfer structs can reasonably expose fields when their schema is intentionally public.
Tests should exercise semantic output, not only compilation. A test asserting request and error counts separately prevents a hurried field rename from returning a plausible but wrong number.
My E0609 checklist
- What exact type did rustc resolve for the receiver?
- Does the active definition contain the field under this target and feature set?
- Is the name misspelled, renamed, nested, generated, or conceptually absent?
- Would a public accessor express a more stable contract?
- Does adding a field change construction, patterns, serialization, or compatibility?
- Is an adapter translating the external schema into the local model?
- Are generated files being fixed at their source rather than by hand?
- Does a semantic test distinguish the desired value from suggested fields?
The core principle is that a field name belongs to a specific compiled schema. I use E0609 to verify the real type and the real domain need before changing syntax. That prevents an easy compiler fix from turning into a quiet data-model mistake.