RFA-465 · Case file with fixtures · Case 437 of 694 · Compiler evidence
Rust Struct Literal Syntax Needs a Resolvable Struct Type
Brace construction must resolve to a declared named-field data type. Verify spelling, imports, qualification, cfg state, and public paths; then initialise the exact current fields rather than inventing a look-alike type.
- 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
- Brace construction requires a declared nominal named-field identity; matching field labels cannot create or infer an anonymous record type.
- First discriminating check
- Resolve the intended current public path and shape, checking imports and cfg boundaries before declaring any new look-alike type.
I wrote Packet { id: 7 } before any Packet type was declared or imported. Rust produced E0422 because brace construction needs a resolvable struct, struct-like enum variant, or union.
The failing fixture is simple, but this diagnostic often appears in larger code after a type moves modules, becomes feature-gated, or changes representation.
Braces describe named-field construction
The identifier before { ... } is not inferred from the assigned variable name or the listed fields. Rust resolves it as a type or variant path whose declared named fields determine whether the literal is valid.
The E0422 explanation distinguishes an undefined identifier from an existing local value that is not a struct type. Both fail because expression syntax cannot create a new anonymous record shape.
Rust uses nominal data types: matching field names alone do not establish identity.
Declare or import the intended data model
The repaired fixture declares:
struct Packet {
id: u64,
}
The literal now has a known layout and field contract. The struct item Reference defines named-field, tuple, and unit structs.
In application code, I normally locate the existing domain type rather than adding a second local Packet just to satisfy resolution.
Qualify enum struct variants
For enum Event { Packet { id: u64 } }, the constructor path is Event::Packet { id: 7 }. Importing the variant can shorten it, but qualification often keeps protocol transitions easy to read.
I inspect whether a similarly named struct and variant coexist. Auto-importing the wrong one may lead to a cascade of misleading field errors.
The resolved item must be both visible and the intended identity.
Check module and feature boundaries
An import in a parent module does not automatically create the same short name in a nested child. Conditional compilation can also remove either the definition or its re-export for one target.
I compare the failing build's features and target with editor configuration. If the type is optional, code constructing it likely needs the same cfg boundary or a stable abstraction available in every configuration.
Creating a fallback type with the same fields usually hides the configuration error.
Shape changes require a different constructor
If Packet changed from a named-field struct to struct Packet(u64), brace syntax is no longer correct. If fields became private, downstream code cannot construct it directly even when the type resolves.
I use the crate's public constructor or builder when direct structure is intentionally hidden. This preserves validation and forward compatibility.
The struct-expression Reference explains field expressions and update syntax once the type is known.
Type inference works inside, not before, identity
Rust may infer the type of 7 from the declared id: u64 field. It cannot infer which Packet declaration I intended from id alone.
This ordering matters when several record types share common field labels. I resolve the nominal path first, then interpret field values under that declaration.
For generic field expressions, explicit conversion at the boundary can improve diagnostics.
Generated models need stable paths
Schema generators may emit a struct in a versioned module while templates construct an older unqualified name. I keep the generated model path in one mapping and test a real construction for each emitted record kind.
When schema names normalise or collide, the generator should report the original identities rather than create ambiguous short imports.
My E0422 checklist
When this appears in tests, I check whether the test was relying on a private implementation detail. Moving a test from an internal module to an integration-test crate changes privacy and import scope. The right repair may be to exercise a public constructor, not make fields public or duplicate the internal struct. Keeping that boundary intact gives the test more realistic evidence about what downstream users can actually build.
- Does the path resolve to any item in this module?
- Is the intended item a named-field struct, variant, or union?
- Did an import, re-export, module move, or cfg feature change?
- Am I about to declare a duplicate look-alike instead of using the real type?
- Did the type change to tuple or unit shape?
- Are fields public and directly constructible here?
- Should a validated constructor or builder be used?
- Does generated code use the current versioned path?
The core principle is that a field list does not define an anonymous type in Rust. Brace construction starts from one declared nominal identity. I resolve that identity and its public contract before reasoning about field values.