You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bevy_reflect's trait surface (TypePath, Reflect, GetTypeRegistration, Typed, FromReflect) is currently bundled with the runtime (TypeRegistry, dynamic types, serde integration, foreign-impls). Several open upstream conversations are converging on the same structural fix: extract the trait definitions from the runtime so domain crates, asset sub-crates, no_std targets, and inspector-adjacent tooling can implement the traits without paying for the runtime they don't use.
This proposal recommends leading with that extraction (bevy_reflect_traits) and complementing it with two consumer-side knobs — a sealed MarkerType blanket and the #[type_path(ignore_params(T))] derive attribute already sketched by MrGVSV in #9094. The combination resolves a class of bug rather than a single use case: optional-reflection (bevyengine/bevy#20337, #22721), bevy_image-style sub-crates that want TypePath without the runtime (#18501), and the foreign-marker generic-parameter wall hit by typed-quantity / phantom-frame domain crates (#9094, #19171).
The original framing of this issue — "foreign-crate marker types as TypePath-bound generic parameters" — survives as one motivating use case among several. The investigation appended at the bottom (Q1–Q4) is the audit trail that produced this recalibration.
Concrete reproduction
// foreign-crate (Bevy-free)pubtraitPlanet:'static{}pubstructEarth;implPlanetforEarth{}pubstructPlanetInertial<P:Planet>(core::marker::PhantomData<P>);pubstructPosition<F>(/* ... */);// consumer crate (Bevy-aware)use bevy::prelude::*;use bevy::reflect::Reflect;use foreign_crate::{Earth,Planet,PlanetInertial,Position};#[derive(Component,Reflect)]#[reflect(opaque,Component)]pubstructTranslationalStateC<P:Planet>(pubPosition<PlanetInertial<P>>);// ^^^^^^^^^^// The `#[derive(Reflect)]` expands to roughly// `impl<P: TypePath + Planet> Reflect for TranslationalStateC<P>`,// so `TranslationalStateC<Earth>` requires `Earth: TypePath`.// Trying to bridge in the consumer:impl bevy::reflect::TypePathfor foreign_crate::Earth{/* ... */}// ^^^ error[E0210]: type parameter `Earth` must be used as the type// parameter for some local type — `TypePath` is foreign and// `Earth` is foreign. Orphan rule.
There is no purely-local fix. The consumer crate is stuck, and the foreign crate cannot help without violating its Bevy-free contract.
Why this matters
Phantom-typed domain crates are how modern Rust gets compile-time unit/frame safety.uom, dimensioned, our own jeod_quantities, and any astro_* / physics_* crate that lifts JEOD-style frame discipline into the type system follow the same pattern: small, dependency-light core; marker types parameterizing dimensional/coordinate quantities; tests that prove arithmetic preserves units/frames.
The three-layer rule is structural, not stylistic. This project keeps physics in jeod_* (zero Bevy dep) and Bevy wiring in bevy_jeod so the same physics runs unchanged in a non-Bevy jeod_runner, batch propagation, and (future) WASM. See CLAUDE.md §"Three-Layer Architecture (non-negotiable)". Adding bevy_reflect to the physics crate forces every non-Bevy consumer to compile and link the reflection runtime, which we explicitly want to avoid.
The current workaround silently downgrades type safety. We compile our Bevy components with a <SelfPlanet> wildcard (src/components.rs) and recover planet identity at runtime via a sibling IntegSourceC component plus an entity hierarchy. Mission code that wants compile-time planet identity (chief on Mars, deputy on Earth in the same World) has to drop off-component into a typed sibling. See simnaut/bevy_jeod#300 for the concrete tracking issue and simnaut/bevy_jeod#296/#315/#316 for the design history.
The pain is not specific to typed-quantity crates. The same wall is hit by bevy_image (#18501, asset sub-crate that wants TypePath without the runtime), by the binary-size / compile-time concern that motivates #20337 and #22721, by no_std / WASM targets, and by inspector-adjacent crates that want to publish reflectable types without committing every consumer to the full reflection runtime. Each of these is a different consumer, but the same trait/runtime coupling is in their way.
Existing workarounds (and their costs)
Wildcard phantom + runtime planet keying. Pin the component's phantom to a self-referential marker (SelfPlanet); recover identity at runtime via a sibling component or entity-tree walk. Works today, but loses compile-time planet identity at the ECS layer; cross-planet bugs become runtime asserts instead of compile errors.
Feature-gate bevy_reflect in the foreign crate. Add an opt-in bevy_reflect feature that emits TypePath/Reflect impls only when enabled. Works, but bends the architectural intent: the foreign crate now compiles bevy_reflect for Bevy consumers, and the cargo feature unification rule means any downstream binary that pulls in both the Bevy adapter and the bare physics crate will pay the cost.
Local newtype wrappers in the consumer. Wrap Earth, Mars, Moon, etc. in consumer-crate newtypes and derive TypePath on those. Doubles the marker surface and forces every consumer to re-export them; harms ergonomics in mission code that wants to write Position<PlanetInertial<Earth>>.
#[reflect_remote] attribute. Bevy already has #[reflect_remote], which generates a repr(transparent) newtype + Reflect impls for a foreign type. Works for concrete foreign types, but does not solve the generic-parameter problem: TranslationalStateC<Earth> still needs Earth: TypePath, not Earth_Wrapper: TypePath. (reflect_remote solves the dual problem — using a foreign type as a field, not as a generic parameter.)
In addition, bevyengine/bevy#9094 (re-opened by MrGVSV) documents three further consumer-side options upstream:
#[type_path(unstable)] — opt-in core::any::type_name fallback per the entire type (matches the spirit of option A below).
#[type_path(ignore_params(T))] — exclude individual generic parameters from the path (added below as a new option).
None of these has landed in bevy_reflect. The thread has been quiet since 2023 but the maintainer interest is on the record.
Bevy precedent: how glam, uuid, petgraph, wgpu_types are reflected
These foreign crates' types appear reflected in Bevy because bevy_reflect/src/impls/ ships hand-written impl_reflect! / impl_reflect_opaque! invocations for them. This works only because bevy_reflect is the trait-defining crate — it can write impl Reflect for ::petgraph::graph::DiGraph<...> without an orphan-rule violation. End-user crates have no equivalent. The current "Bevy ships impls for foreign-crate types" pattern is therefore not extensible: it scales by Bevy maintainers adopting each foreign crate one at a time. A small, generic-marker-laden domain crate has no path to that table.
Proposed options for upstream
The following are not mutually exclusive; (B) and (D) compose, as do (A) and (E). After the investigation, (B) is the headline option; (D) and (G) are the lightest-weight complements that make (B) actually solve the foreign-marker case.
(B) Split a bevy_reflect_traits sub-crate [headline]
Extract just the TypePath, Reflect, GetTypeRegistration, Typed, FromReflecttrait definitions (and their derive-companion bevy_reflect_derive) into a hyper-minimal crate with no runtime, no TypeRegistry, no serde, no bevy_platform dependency. Domain crates, asset sub-crates, and inspector-adjacent crates could optionally depend on this with negligible compile-time and binary cost — closer to the cost of depending on serde_derive than on full serde.
This is analogous to what already happened with bevy_reflect_derive (proc-macro-only sibling). The trait definitions themselves are tiny: type_path.rs is 243 lines, reflect.rs is mostly trait sigs. The bulk of bevy_reflect's compile cost is the TypeRegistry, serde integration, func reflection, path reflection, and the foreign-crate impls/.
This option is already an open upstream conversation. bevyengine/bevy#20337 (S-Ready-For-Implementation, opened by alice-i-cecile) and #22721 (open PR adding enable_codegen) are pursuing the optional-reflection refactor at the runtime layer. Reviewer ashivaram23 on #22721 raised the trait-extraction question verbatim: "Should TypePath be separated from this, since it is used independent of the rest of reflection over Bevy? Mainly for Asset like in #18501." Bevy itself has internal precedent — bevy_image (#18501) wants TypePath without the rest of bevy_reflect's runtime, currently satisfied with a feature-gated workaround.
Pro: orthogonal to scene stability; surgical fix; matches the existing _derive extraction pattern; lands directly in an in-progress refactor; benefits multiple independent use cases (asset crates, optional reflection, foreign markers, no_std).
Con: still requires a (very thin) Bevy dep in the foreign crate. Some "no Bevy at all" crates may still object on principle, but the cost is small enough that the principle becomes pragmatically negotiable.
(D) Marker-trait escape hatch (composes with B)
In bevy_reflect_traits (option B), add a sealed pub trait MarkerType: 'static {} with a blanket impl<T: MarkerType> TypePath for T that returns core::any::type_name::<T>(). Foreign crates that opt in via impl bevy_reflect_traits::MarkerType for Earth {} (orphan-legal: MarkerType is foreign but Earth is local) get TypePath for free. This narrowly targets the trivial-unit-struct phantom marker case and accepts the type_name instability tradeoff for that narrow case only.
Pro: Composes cleanly with (B); explicitly opt-in; user crate decides per-marker.
Con: still requires the (thin) bevy_reflect_traits dep. Dead in the water without (B).
(Added per MrGVSV's sketch in #9094.) Extend #[derive(Reflect)] with an attribute that omits a generic parameter from the derived type_path() string entirely:
The expansion drops the P: TypePath bound and emits a path string that does not encode P. Two TranslationalStateC<Earth> and TranslationalStateC<Mars> instances therefore share the same type_path().
Pro: pure consumer-side fix; no foreign-crate cooperation; no orphan-rule fight; cheap to implement on top of the existing derive.
Con: collapses the type-registry distinction between different parameter instantiations. Acceptable for inspector / runtime-only use cases where the generic identity does not need to round-trip through serialized scenes, not acceptable when the parameter must be reflected (e.g., scene round-trip needs to distinguish TranslationalStateC<Earth> from TranslationalStateC<Mars>). This is the right tool only when the parameter is genuinely a phantom-marker that the registry doesn't need to see.
(A) BasicTypePath blanket via core::any::type_name
Provide a bevy_reflect::BasicTypePath opt-in trait with a blanket impl<T: BasicTypePath> TypePath for T whose type_path() returns core::any::type_name::<T>(). Foreign markers satisfy the bound by being 'static; the consumer adds impl bevy_reflect::BasicTypePath for ForeignType {} from inside the consumer crate (legal because BasicTypePath is bevy_reflect's trait — wait, that's the orphan-rule problem again unless the trait is structured like Sealed and the impl can be auto-generated).
A cleaner variant: bevy_reflect provides an unconditional default impl for trivially-marker-shaped types via a special MarkerTypePath auto-trait or 'static + Sized + ZeroSized blanket. Tradeoffs:
Pro: zero foreign-crate cooperation needed.
Con: core::any::type_name is documented as not stable across rustc versions or rebuilds with different cargo flags. Bevy explicitly chose stable derive-generated strings over type_name (type_path.rs L14–L25) precisely because scene de/serialization keys off these strings.
Mitigation: gate the blanket behind a "transient reflection only" marker. Acceptable for inspectors and runtime-only uses; not safe for serialized scenes.
(C) #[reflect(via = LocalShim<ForeignParam>)] attribute on the consumer [breaking; major-version only]
Extend the consumer-side #[derive(Reflect)] with an attribute that lets the consumer designate a local newtype shim for a foreign generic parameter:
The macro generates MyShim<P> as a local newtype (no orphan-rule issue) and threads its TypePath impl through the derived component impl in place of P's. The end user never writes the shim type by hand.
The investigation appended below (Q2) confirms this is scene-format-breaking: the registered type_path() string changes from crate::TranslationalStateC<earth_crate::Earth> to crate::TranslationalStateC<consumer_crate::EarthShim>, and the deserializer keys off that string. Any scene written before the swap fails to deserialize after. Therefore this option can ship only as an explicitly-breaking option targeting a Bevy major-version release, with a migration story.
Pro: purely consumer-side fix; no foreign-crate cooperation; preserves the stable-string contract for the new path.
Today TypeRegistration::of::<T>() requires T: TypePath (type_registry.rs L195–L245). Add a TypeRegistry::register_with_explicit_path::<T>(path: &'static str, ...) (or register_dynamic) that constructs a TypeRegistration from caller-supplied strings and a manually-built type-info table, skipping the TypePath bound entirely.
Pro: Pure runtime; foreign markers stay 100% Bevy-free; the consumer crate registers Earth/Mars/Moon at app-init time with literal strings.
Con: bypasses the derive(Reflect) ergonomics — every component generic over a foreign marker would need a hand-rolled Reflect impl that doesn't depend on P: TypePath at the trait-bound level, which is a much bigger surgery than just relaxing the registration entry point. Probably only useful as a complement to one of the static options above.
(F) [Dismissed] Generative reflect_foreign!(Earth) macro on the consumer
A macro that the consumer invokes to emit impl TypePath for ForeignType {} from inside the consumer crate. This still produces an impl ForeignTrait for ForeignType and hits the orphan rule — Rust's coherence checker is unimpressed by which macro authored the tokens. Dismissed. It only works with a Rust language change (e.g., scoped/local impls), which is out of scope here.
Recommendation
Lead with (B). The bevy_reflect_traits extraction is the structural fix that lands in an already-active upstream conversation (#20337, #22721, #18501); foreign-marker support is one payoff among several, not the load-bearing justification.
Complement with (D) and (G) for full coverage of the foreign-marker case:
(D) MarkerType blanket — for trivial unit-struct markers that should appear in the registry by their type_name string, when the consumer wants compile-time identity preserved.
(G) #[type_path(ignore_params(T))] — for phantom parameters the registry doesn't need to see at all, when the consumer is happy with a single shared type_path() across instantiations.
(A) and (E) are useful escape valves for ad-hoc and inspector-only cases; ship if cheap, skip if not.
(C) is breaking-change-only per Q2 — it changes the registered type_path() string of every type using the wrapped parameter, breaking scene round-trip. Park for a major-version release if pursued at all.
(F) is dead.
Upstream engagement strategy
Engage with existing upstream threads rather than filing a new issue. The investigation (Q1, Q4) found maintainer activity already pointed at this problem space, and a fresh issue would either look like inadequate prior-art search or fragment the conversation across three threads.
The two entry points:
bevyengine/bevy#9094 — "TypePath required for generic #[reflect(ignore)] field". Re-opened by bevy_reflect maintainer MrGVSV with a three-option sketch (newtype wrap, #[type_path(unstable)], #[type_path(ignore_params(T))]). Quiet since 2023, but the maintainer interest is on the record. Add a comment that credits MrGVSV's three options, contributes the foreign-marker-as-generic-parameter framing with a concrete typed-quantity / phantom-frame repro, and points at #22721 for the structural extraction conversation.
bevyengine/bevy#22721 — "Make reflection optional" (open PR). Reviewer ashivaram23 raised: "Should TypePath be separated from this, since it is used independent of the rest of reflection over Bevy? Mainly for Asset like in #18501." Add a +1 comment with substance: the bevy_image precedent, the foreign-marker generic-parameter case as a second independent motivator, and a sketch of the extraction's scope (which traits move, which stays in bevy_reflect).
bevyengine/bevy#20337 — S-Ready-For-Implementation, D-Modest, opened by alice-i-cecile. Optional reflection at the bevy level. The trait-extraction work in #22721 is the implementation of this; comment if the trait-vs-runtime split needs further alignment.
The two upstream-facing comment drafts are posted as comments on this issue (simnaut/bevy_jeod#321) for review before being copied upstream.
Open questions — investigation
The four questions originally listed at the bottom of this proposal were investigated against the upstream bevyengine/bevy issue tracker (open + closed), the published bevy_reflect 0.18.1 source, and Bevy's migration guides / docs / cheatbook. Findings below; no upstream maintainer has been contacted.
Q1 — Has this been discussed upstream before?
Answer: yes, the symptom has been raised multiple times and a maintainer has sketched essentially the same option-space we did, but no upstream issue or PR specifically frames it as "foreign marker types as generic parameters" or proposes a structural fix. The closest active thread is bevyengine/bevy#9094 (re-opened by MrGVSV, a bevy_reflect maintainer); the proposal here is a strict superset of that thread.
Relevant upstream artifacts, in approximate order of similarity to this proposal:
bevyengine/bevy#9094 — "TypePath required for generic #[reflect(ignore)] field" (open, re-opened by MrGVSV, A-Reflection). The strongest match. OP hits the exact same wall (leafwing-input-manager's ActionState<A: Actionlike> cannot be registered because A: TypePath is required even with #[reflect(ignore)]). Initial maintainer answer: "this cannot be lifted." Then re-opened by MrGVSV with three options:
Wrap foreign types in local newtypes (≈ this proposal's option 3 / option C).
Add #[type_path(unstable)] opt-out using core::any::type_name (≈ this proposal's option A / option D).
Add #[type_path(ignore_params(T))] to omit a parameter from the path (≈ this proposal's option G).
No upstream PR has landed any of these. The thread has been quiet since 2023. The proposal here is not a duplicate; it expands MrGVSV's option-space and adds the structural bevy_reflect_traits split (option B) and the runtime-registry escape (option E), neither of which appear in #9094.
bevyengine/bevy#19171 — "Error registering generic type with PhantomData" (open, 2026, C-BugA-Reflection). Same root cause, narrower framing — register_type::<Foo<T>>() fails because T: TypePath + Send + Sync. No proposed fix; just a bug-report.
bevyengine/bevy#18606 — "Having more than two types param in a type and deriving Reflect errors" (closed, dup of bodil/smartstring#7). Different problem (where-clause expansion bug), surfaced through the same T: TypePath wall.
bevyengine/bevy#5144 / #5145 — "PhantomData does not implement Reflect" (closed S-Wontfix then partly addressed). Resolved at the level of PhantomData<T> itself, not at the level of T.
bevyengine/bevy#9929 — "implement Reflect for Box<T>" (open). Adjacent: foreign-type-as-field, not foreign-type-as-generic-parameter.
bevyengine/bevy#5432 — closed. About TypeUuid (predecessor of TypePath) on primitives in generics; superseded by stable type path v2.
bevyengine/bevy/discussions#2570 — "Ergonomics of implementing Reflect for complex and external types." Discusses foreign-type-as-field; the resolution path was #[reflect_remote] (PR #6042). Does not address foreign-type-as-generic-parameter.
bevyengine/bevy/discussions#8349 — "Reflection for External Crates." Same scope as #2570; recommends bevy_mod_component_mirror for foreign types as fields.
bevyengine/bevy#12691 — "Make Typed, TypePath, GetTypeRegistration, etc supertraits of Reflect" (open). Tangential — about whether T: Reflect should imply T: TypePath. Doesn't address the foreign-marker case.
bevyengine/bevy#7184 — "reflect: stable type path v2" (merged). The PR that introduced TypePath and the recursive composition that creates the orphan-rule pinch we hit. Establishes the design rationale: stable serialized scenes require stable strings, which is why core::any::type_name was rejected.
The two issues this proposal originally cited (#5146, #7775) are about stable-string generation generally, not the foreign-marker-as-generic-parameter case — that distinction holds up.
Course correction recommended: the upstream-facing version of this proposal should explicitly open with a reference to #9094 and credit MrGVSV's three-option sketch, then position our additions (B = trait-only sub-crate; E = runtime registry; explicit framing as a class-of-bug rather than per-crate workaround) as the unhandled half of that conversation. Not a perfect duplicate — no need to pivot to a +1. But pretending #9094 doesn't exist will look like inadequate prior-art search. Add MrGVSV's option 3 (#[type_path(ignore_params(T))]) as a fourth consumer-side option (now option G above).
Q2 — Does the #[reflect(via_param = LocalShim<P>)] attribute (option C) break scene round-trip?
Answer: yes, definitively. Switching a generic parameter from Earth to EarthShim mid-flight changes the parent struct's registered type_path() string, and that string is the registry key the scene deserializer looks up. Option C cannot be sold as a transparent ergonomic fix; it would have to be marked as a breaking format change.
Evidence, all from bevy_reflect 0.18.1 in the local cargo cache:
The derive expansion for TypePath::type_path() literally concatenates each generic parameter's type_path(): bevy_reflect_derive-0.18.1/src/derive_data.rs:1273-1305 — long_type_path() calls reduce_generics() which emits <#ident as TypePath>::type_path() for each TypeParam, joined into module::Outer<param1, param2, ...>. Same pattern at lines 1310-1341 for short_type_path(). So TranslationalStateC<Earth>::type_path() is verbatim "crate::TranslationalStateC<earth_crate::Earth>" — change Earth to EarthShim and the parent path becomes "crate::TranslationalStateC<consumer_crate::EarthShim>".
The TypeRegistry is keyed by that exact type_path() string for cross-type lookup: bevy_reflect-0.18.1/src/type_registry.rs:326 — type_path_to_id.insert(registration.type_info().type_path(), registration.type_id()).
The serde deserializer reads exactly that string off the wire and looks it up in type_path_to_id: bevy_reflect-0.18.1/src/serde/de/registrations.rs:7-12, 40-47 — doc: "This deserializer expects a string containing the full [type path] of the type to find the TypeRegistration of." Code: self.0.get_with_type_path(type_path). The bevy_reflect::TypePath doc itself reinforces this stability contract at bevy_reflect-0.18.1/src/type_path.rs:14-25 ("(de)serialization, rely on type paths as identifiers for matching dynamic values to concrete types").
For comparison, the existing #[reflect_remote] attribute (bevy_reflect-0.18.1/src/remote.rs, bevy_reflect_derive-0.18.1/src/remote.rs:139-212) works at the field level: it generates impl ReflectRemote for LocalShim with the shim'sTypePath, and the parent struct's type_path() includes the shim's path because that's what its field is. There is no machinery for surfacing the underlying foreign type's path through a wrapper. Once a remote-shim is the field type, it is the registered type — and any scene written before the swap will key against the foreign type's path and fail to deserialize after the swap. Same algebra applies for the proposed via_param extension at the generic-parameter level.
Implication for the proposal: option C is not a transparent ergonomic fix. It is a scene-format-breaking rename of every type using the wrapped parameter. It can still ship — but as an explicitly-breaking option targeting Bevy's next major release, with a migration story. The proposal already flagged this risk; this investigation upgrades it from "needs verification" to "confirmed breaking." Recommend marking option C [breaking; major-version only] in the recommendation table.
Answer: no. There is no documented Bevy-blessed pattern for "I want generic component discrimination, but use a wildcard + sibling marker instead." The official documented workaround is option 3 from this proposal — the local newtype wrapper. Lifting our wildcard pattern into a Bevy idiom is therefore a real opportunity, but it is new documentation, not codification of an existing one.
Evidence:
Bevy's official migration guide for the stable type path v2 transition (https://bevy.org/learn/migration-guides/0-10-to-0-11/) prescribes a single workaround for the foreign-generic-parameter case, verbatim: "If you don't own a type you may need to wrap it in a newtype and manually implement TypePath for the newtype." That is the local-newtype-wrapper option (this proposal's option 3 / a stripped-down option C). It is the only documented workaround.
The Unofficial Bevy Cheat Book's pages on Generic Systems and Entities, Components document Component<T> patterns and marker components, but contain zero references to the wildcard-plus-discriminator idiom or to type-erasure as a workaround for generic-component reflection.
tainted-coders.com/bevy/components — same: covers marker components and PhantomData<T> parametric components, no wildcard-plus-discriminator pattern.
Bevy itself does not use a wildcard-plus-discriminator pattern in any first-party crate I checked (bevy_animation, bevy_asset, bevy_pbr, bevy_render, bevy_ecs). Bevy's own parametric components (Material<T>, AssetLoader<T>) require T: TypePath and accept that downstream users lose any T that isn't owned-or-Reflect-able.
A community crate (bevy_mod_erased_component_registry) implements something near this idiom — type-erased components keyed by a sibling TypeId — but it is third-party, niche, and not referenced from any official Bevy doc.
The maintainer in bevyengine/bevy#9094 proposes the wrapper pattern as the fix and never mentions a wildcard-plus-discriminator alternative. None of the comment threads on #19171, #18606, #5144, #9094 suggest it either.
Implication for the proposal: the wildcard-plus-discriminator pattern this codebase ended up using (<SelfPlanet> + IntegSourceC) is genuinely novel territory upstream. It is a viable alternative to option 3 (manual newtype wrappers) when the consumer would have to write one wrapper per marker — exactly our case (Earth, Mars, Moon, …). Lifting it to a documented idiom + helper macro (something like #[derive(WildcardComponent)] #[discriminator(IntegSourceC)]) would be a real contribution upstream, but it is additive new design, not codification of an existing convention. The original proposal's framing — "if Bevy already has this idiom, document it" — should be updated to "propose this idiom + helper as a new addition; it is currently unmapped."
Q4 — Does bevy_reflect_traits extraction (option B) stand on its own?
Answer: yes — strongly. Independent maintainer-driven work is already in motion to make bevy_reflect optional / decomposable, and at least one upstream reviewer has explicitly raised the question of separating TypePath from the rest of reflection. The trait-only-sub-crate split lands directly in that ongoing conversation; foreign-marker support would be a side-effect, not the primary justification.
Evidence:
bevyengine/bevy#20337 — "Reflection should be fully optional and toggleable at a bevy level" (open, opened by alice-i-cecile, labelled S-Ready-For-Implementation, D-Modest, A-Cross-Cutting). Direct quote: "Reflection results in significant increases to compile times and binary size, and in many projects, is only useful as a development tool." The maintainer-prescribed shape: "bevy_reflect should be an optional dependency for all Bevy crates whenever possible, and we should have a reflect feature flag on bevy itself that controls this. […] this will need to be mirrored to bevy_internal." That is the exact problem option B solves at the _traits level — except option B targets the trait definitions, while #20337 targets the runtime. The two are complementary, not competing.
bevyengine/bevy#22721 — "Make reflection optional" (open PR). Adds enable_codegen feature to bevy_reflect_derive that no-ops the proc macros. Reviewer ashivaram23 raised verbatim: "Should TypePath be separated from this, since it is used independent of the rest of reflection over Bevy? Mainly for Asset like in #18501." This is the specific structural question option B answers. The PR is currently S-Needs-Review / X-Needs-SME, so the discussion is open.
bevyengine/bevy#18501 — closed, "bevy_image: derive TypePath when Reflect is not available." Direct evidence that Bevy itself has a bevy_* crate (bevy_image) that wants TypePath without the rest of bevy_reflect's runtime, and currently has to satisfy that with a feature-gated workaround. Trait-only sub-crate would be the structural fix.
The runtime/trait split is already half-implemented inside bevy_reflect itself: bevy_reflect_derive is a separate proc-macro crate (paralleling the bevy_reflect_traits split this proposal proposes). The trait sigs are a tiny fraction of the crate by line-count: type_path.rs 243 lines, reflect.rs is mostly trait sigs, from_reflect.rs is small. The bulk — TypeRegistry, serde/, func/, path/, impls/{glam,uuid,petgraph,wgpu_types,smallvec,indexmap,smol_str,foldhash,hashbrown}.rs, dynamic* types — is runtime / serialization / foreign-impls. The trait-only crate would be roughly the union of type_path.rs, reflect.rs, reflectable.rs, the supertraits in from_reflect.rs, plus the registration entry-point, with no serde, no impls/, no dynamic types. Conservative estimate: ~10% of bevy_reflect's current line-count and dependency footprint.
Independent use cases that benefit without invoking foreign markers:
bevy_image-style sub-crates (#18501) that want Asset registration via TypePath without paying for bevy_reflect runtime.
The "make reflection optional at the binary level" line of work (#20337, #22721): if the trait crate stays compile-time-cheap, downstream code can keep using T: TypePath bounds even when reflection is feature-gated off. Right now those bounds force the heavy runtime to be present.
Reusable-component / inspector / editor crates (bevy_egui-adjacent, bevy-inspector-egui-adjacent) that want to publish Reflect-able types without committing every consumer to the full runtime.
no_std / WASM / size-constrained targets where the TypeRegistry and serde weight is unwanted but T: TypePath bounds are still desired (the migration guide explicitly says Asset requires TypePath).
Implication for the proposal: option B is the strongest of the five and should be moved to the headline recommendation. It is already an open upstream conversation with explicit maintainer (alice-i-cecile) and reviewer (ashivaram23) interest. Foreign-marker support is one of several payoffs, not the load-bearing justification. The upstream-facing version of this proposal should reframe option B as "complete the optional-reflection refactor that #20337 / #22721 / #18501 are already pursuing, by extracting bevy_reflect_traits as the trait-only crate. As a side effect, this resolves the foreign-marker class of bug raised in #9094 / #19171." That framing converts the proposal from a niche request into the missing piece of an in-progress refactor.
Bottom line on whether to take this upstream
Don't open a new issue cold. The right entry points already exist: #9094 (foreign-marker / generic-parameter case) and #20337 / #22721 (optional-reflection / trait-extraction case). Both threads have maintainer activity and would absorb this proposal naturally.
Recommended path: post a focused comment on #9094 proposing options A–E (crediting MrGVSV's three-option sketch and adding our two), and a parallel comment on #22721 arguing for the trait-only-sub-crate split as the structural form of "make reflection optional." Reference each thread from the other.
Don't pitch option C as transparent ergonomics — Q2 confirms it's scene-breaking. Frame it as a major-version migration option.
Don't position option B as load-bearing on foreign-marker support — Q4 confirms it stands on its own and is already wanted upstream.
Add MrGVSV's #[type_path(ignore_params(T))] from #9094 as a fourth consumer-side option (option G in the structured option list above).
References
simnaut/bevy_jeod#300 — internal tracking issue this proposal addresses (promote TranslationalStateC from <SelfPlanet> wildcard to <P: Planet>).
simnaut/bevy_jeod#296, #315, #316, #304 — wildcard-pattern design history; document the cost we are paying today.
crates/jeod_quantities/src/frame.rs — the Planet trait, define_planet! macro, SelfPlanet, PlanetInertial<P> definitions. Bevy-free by design.
src/components.rs — every Bevy component pins its phantom at the wrapper boundary; TranslationalStateC(pub TranslationalStateTyped<PlanetInertial<SelfPlanet>>).
bevy_reflect/src/impls/ — precedent for reflecting foreign crate types from inside bevy_reflect (glam, uuid, petgraph, wgpu_types); precisely the path that is not available to end-user crates.
bevy_reflect::reflect_remote — solves the dual problem (foreign type as a field), does not help with foreign type as a generic parameter.
Filed in simnaut/bevy_jeod first for review. Upstream-facing draft comments for bevyengine/bevy#9094 and bevyengine/bevy#22721 are posted as comments on this issue.
Summary
bevy_reflect's trait surface (TypePath,Reflect,GetTypeRegistration,Typed,FromReflect) is currently bundled with the runtime (TypeRegistry, dynamic types,serdeintegration, foreign-impls). Several open upstream conversations are converging on the same structural fix: extract the trait definitions from the runtime so domain crates, asset sub-crates,no_stdtargets, and inspector-adjacent tooling can implement the traits without paying for the runtime they don't use.This proposal recommends leading with that extraction (
bevy_reflect_traits) and complementing it with two consumer-side knobs — a sealedMarkerTypeblanket and the#[type_path(ignore_params(T))]derive attribute already sketched byMrGVSVin#9094. The combination resolves a class of bug rather than a single use case: optional-reflection (bevyengine/bevy#20337,#22721),bevy_image-style sub-crates that wantTypePathwithout the runtime (#18501), and the foreign-marker generic-parameter wall hit by typed-quantity / phantom-frame domain crates (#9094,#19171).The original framing of this issue — "foreign-crate marker types as
TypePath-bound generic parameters" — survives as one motivating use case among several. The investigation appended at the bottom (Q1–Q4) is the audit trail that produced this recalibration.Concrete reproduction
There is no purely-local fix. The consumer crate is stuck, and the foreign crate cannot help without violating its Bevy-free contract.
Why this matters
uom,dimensioned, our ownjeod_quantities, and anyastro_*/physics_*crate that lifts JEOD-style frame discipline into the type system follow the same pattern: small, dependency-light core; marker types parameterizing dimensional/coordinate quantities; tests that prove arithmetic preserves units/frames.jeod_*(zero Bevy dep) and Bevy wiring inbevy_jeodso the same physics runs unchanged in a non-Bevyjeod_runner, batch propagation, and (future) WASM. SeeCLAUDE.md§"Three-Layer Architecture (non-negotiable)". Addingbevy_reflectto the physics crate forces every non-Bevy consumer to compile and link the reflection runtime, which we explicitly want to avoid.<SelfPlanet>wildcard (src/components.rs) and recover planet identity at runtime via a siblingIntegSourceCcomponent plus an entity hierarchy. Mission code that wants compile-time planet identity (chief on Mars, deputy on Earth in the sameWorld) has to drop off-component into a typed sibling. Seesimnaut/bevy_jeod#300for the concrete tracking issue andsimnaut/bevy_jeod#296/#315/#316for the design history.bevy_image(#18501, asset sub-crate that wantsTypePathwithout the runtime), by the binary-size / compile-time concern that motivates#20337and#22721, byno_std/ WASM targets, and by inspector-adjacent crates that want to publish reflectable types without committing every consumer to the full reflection runtime. Each of these is a different consumer, but the same trait/runtime coupling is in their way.Existing workarounds (and their costs)
SelfPlanet); recover identity at runtime via a sibling component or entity-tree walk. Works today, but loses compile-time planet identity at the ECS layer; cross-planet bugs become runtime asserts instead of compile errors.bevy_reflectin the foreign crate. Add an opt-inbevy_reflectfeature that emitsTypePath/Reflectimpls only when enabled. Works, but bends the architectural intent: the foreign crate now compilesbevy_reflectfor Bevy consumers, and thecargofeature unification rule means any downstream binary that pulls in both the Bevy adapter and the bare physics crate will pay the cost.Earth,Mars,Moon, etc. in consumer-crate newtypes and deriveTypePathon those. Doubles the marker surface and forces every consumer to re-export them; harms ergonomics in mission code that wants to writePosition<PlanetInertial<Earth>>.#[reflect_remote]attribute. Bevy already has#[reflect_remote], which generates arepr(transparent)newtype + Reflect impls for a foreign type. Works for concrete foreign types, but does not solve the generic-parameter problem:TranslationalStateC<Earth>still needsEarth: TypePath, notEarth_Wrapper: TypePath. (reflect_remotesolves the dual problem — using a foreign type as a field, not as a generic parameter.)In addition,
bevyengine/bevy#9094(re-opened byMrGVSV) documents three further consumer-side options upstream:#[type_path(unstable)]— opt-incore::any::type_namefallback per the entire type (matches the spirit of option A below).#[type_path(ignore_params(T))]— exclude individual generic parameters from the path (added below as a new option).None of these has landed in
bevy_reflect. The thread has been quiet since 2023 but the maintainer interest is on the record.Bevy precedent: how
glam,uuid,petgraph,wgpu_typesare reflectedThese foreign crates' types appear reflected in Bevy because
bevy_reflect/src/impls/ships hand-writtenimpl_reflect!/impl_reflect_opaque!invocations for them. This works only becausebevy_reflectis the trait-defining crate — it can writeimpl Reflect for ::petgraph::graph::DiGraph<...>without an orphan-rule violation. End-user crates have no equivalent. The current "Bevy ships impls for foreign-crate types" pattern is therefore not extensible: it scales by Bevy maintainers adopting each foreign crate one at a time. A small, generic-marker-laden domain crate has no path to that table.Proposed options for upstream
The following are not mutually exclusive; (B) and (D) compose, as do (A) and (E). After the investigation, (B) is the headline option; (D) and (G) are the lightest-weight complements that make (B) actually solve the foreign-marker case.
(B) Split a
bevy_reflect_traitssub-crate [headline]Extract just the
TypePath,Reflect,GetTypeRegistration,Typed,FromReflecttrait definitions (and theirderive-companionbevy_reflect_derive) into a hyper-minimal crate with no runtime, noTypeRegistry, noserde, nobevy_platformdependency. Domain crates, asset sub-crates, and inspector-adjacent crates could optionally depend on this with negligible compile-time and binary cost — closer to the cost of depending onserde_derivethan on fullserde.This is analogous to what already happened with
bevy_reflect_derive(proc-macro-only sibling). The trait definitions themselves are tiny:type_path.rsis 243 lines,reflect.rsis mostly trait sigs. The bulk ofbevy_reflect's compile cost is theTypeRegistry,serdeintegration,funcreflection,pathreflection, and the foreign-crateimpls/.This option is already an open upstream conversation.
bevyengine/bevy#20337(S-Ready-For-Implementation, opened byalice-i-cecile) and#22721(open PR addingenable_codegen) are pursuing the optional-reflection refactor at the runtime layer. Reviewerashivaram23on#22721raised the trait-extraction question verbatim: "ShouldTypePathbe separated from this, since it is used independent of the rest of reflection over Bevy? Mainly for Asset like in#18501." Bevy itself has internal precedent —bevy_image(#18501) wantsTypePathwithout the rest ofbevy_reflect's runtime, currently satisfied with a feature-gated workaround._deriveextraction pattern; lands directly in an in-progress refactor; benefits multiple independent use cases (asset crates, optional reflection, foreign markers,no_std).(D) Marker-trait escape hatch (composes with B)
In
bevy_reflect_traits(option B), add a sealedpub trait MarkerType: 'static {}with a blanketimpl<T: MarkerType> TypePath for Tthat returnscore::any::type_name::<T>(). Foreign crates that opt in viaimpl bevy_reflect_traits::MarkerType for Earth {}(orphan-legal:MarkerTypeis foreign butEarthis local) getTypePathfor free. This narrowly targets the trivial-unit-struct phantom marker case and accepts thetype_nameinstability tradeoff for that narrow case only.bevy_reflect_traitsdep. Dead in the water without (B).(G)
#[type_path(ignore_params(T))]consumer-side derive attribute(Added per
MrGVSV's sketch in#9094.) Extend#[derive(Reflect)]with an attribute that omits a generic parameter from the derivedtype_path()string entirely:The expansion drops the
P: TypePathbound and emits a path string that does not encodeP. TwoTranslationalStateC<Earth>andTranslationalStateC<Mars>instances therefore share the sametype_path().TranslationalStateC<Earth>fromTranslationalStateC<Mars>). This is the right tool only when the parameter is genuinely a phantom-marker that the registry doesn't need to see.(A)
BasicTypePathblanket viacore::any::type_nameProvide a
bevy_reflect::BasicTypePathopt-in trait with a blanketimpl<T: BasicTypePath> TypePath for Twhosetype_path()returnscore::any::type_name::<T>(). Foreign markers satisfy the bound by being'static; the consumer addsimpl bevy_reflect::BasicTypePath for ForeignType {}from inside the consumer crate (legal becauseBasicTypePathisbevy_reflect's trait — wait, that's the orphan-rule problem again unless the trait is structured likeSealedand the impl can be auto-generated).A cleaner variant:
bevy_reflectprovides an unconditional default impl for trivially-marker-shaped types via a specialMarkerTypePathauto-trait or'static + Sized + ZeroSizedblanket. Tradeoffs:core::any::type_nameis documented as not stable across rustc versions or rebuilds with different cargo flags. Bevy explicitly chose stable derive-generated strings overtype_name(type_path.rs L14–L25) precisely because scene de/serialization keys off these strings.(C)
#[reflect(via = LocalShim<ForeignParam>)]attribute on the consumer [breaking; major-version only]Extend the consumer-side
#[derive(Reflect)]with an attribute that lets the consumer designate a local newtype shim for a foreign generic parameter:The macro generates
MyShim<P>as a local newtype (no orphan-rule issue) and threads itsTypePathimpl through the derived component impl in place ofP's. The end user never writes the shim type by hand.The investigation appended below (Q2) confirms this is scene-format-breaking: the registered
type_path()string changes fromcrate::TranslationalStateC<earth_crate::Earth>tocrate::TranslationalStateC<consumer_crate::EarthShim>, and the deserializer keys off that string. Any scene written before the swap fails to deserialize after. Therefore this option can ship only as an explicitly-breaking option targeting a Bevy major-version release, with a migration story.(E) Runtime
TypeRegistry::register_with_pathAPIToday
TypeRegistration::of::<T>()requiresT: TypePath(type_registry.rsL195–L245). Add aTypeRegistry::register_with_explicit_path::<T>(path: &'static str, ...)(orregister_dynamic) that constructs aTypeRegistrationfrom caller-supplied strings and a manually-built type-info table, skipping theTypePathbound entirely.derive(Reflect)ergonomics — every component generic over a foreign marker would need a hand-rolledReflectimpl that doesn't depend onP: TypePathat the trait-bound level, which is a much bigger surgery than just relaxing the registration entry point. Probably only useful as a complement to one of the static options above.(F) [Dismissed] Generative
reflect_foreign!(Earth)macro on the consumerA macro that the consumer invokes to emit
impl TypePath for ForeignType {}from inside the consumer crate. This still produces animpl ForeignTrait for ForeignTypeand hits the orphan rule — Rust's coherence checker is unimpressed by which macro authored the tokens. Dismissed. It only works with a Rust language change (e.g., scoped/local impls), which is out of scope here.Recommendation
Lead with (B). The
bevy_reflect_traitsextraction is the structural fix that lands in an already-active upstream conversation (#20337,#22721,#18501); foreign-marker support is one payoff among several, not the load-bearing justification.Complement with (D) and (G) for full coverage of the foreign-marker case:
MarkerTypeblanket — for trivial unit-struct markers that should appear in the registry by theirtype_namestring, when the consumer wants compile-time identity preserved.#[type_path(ignore_params(T))]— for phantom parameters the registry doesn't need to see at all, when the consumer is happy with a single sharedtype_path()across instantiations.(A) and (E) are useful escape valves for ad-hoc and inspector-only cases; ship if cheap, skip if not.
(C) is breaking-change-only per Q2 — it changes the registered
type_path()string of every type using the wrapped parameter, breaking scene round-trip. Park for a major-version release if pursued at all.(F) is dead.
Upstream engagement strategy
Engage with existing upstream threads rather than filing a new issue. The investigation (Q1, Q4) found maintainer activity already pointed at this problem space, and a fresh issue would either look like inadequate prior-art search or fragment the conversation across three threads.
The two entry points:
bevyengine/bevy#9094— "TypePath required for generic#[reflect(ignore)]field". Re-opened bybevy_reflectmaintainerMrGVSVwith a three-option sketch (newtype wrap,#[type_path(unstable)],#[type_path(ignore_params(T))]). Quiet since 2023, but the maintainer interest is on the record. Add a comment that credits MrGVSV's three options, contributes the foreign-marker-as-generic-parameter framing with a concrete typed-quantity / phantom-frame repro, and points at#22721for the structural extraction conversation.bevyengine/bevy#22721— "Make reflection optional" (open PR). Reviewerashivaram23raised: "ShouldTypePathbe separated from this, since it is used independent of the rest of reflection over Bevy? Mainly for Asset like in#18501." Add a +1 comment with substance: thebevy_imageprecedent, the foreign-marker generic-parameter case as a second independent motivator, and a sketch of the extraction's scope (which traits move, which stays inbevy_reflect).bevyengine/bevy#20337—S-Ready-For-Implementation,D-Modest, opened byalice-i-cecile. Optional reflection at thebevylevel. The trait-extraction work in#22721is the implementation of this; comment if the trait-vs-runtime split needs further alignment.The two upstream-facing comment drafts are posted as comments on this issue (
simnaut/bevy_jeod#321) for review before being copied upstream.Open questions — investigation
The four questions originally listed at the bottom of this proposal were investigated against the upstream
bevyengine/bevyissue tracker (open + closed), the publishedbevy_reflect0.18.1 source, and Bevy's migration guides / docs / cheatbook. Findings below; no upstream maintainer has been contacted.Q1 — Has this been discussed upstream before?
Answer: yes, the symptom has been raised multiple times and a maintainer has sketched essentially the same option-space we did, but no upstream issue or PR specifically frames it as "foreign marker types as generic parameters" or proposes a structural fix. The closest active thread is
bevyengine/bevy#9094(re-opened byMrGVSV, abevy_reflectmaintainer); the proposal here is a strict superset of that thread.Relevant upstream artifacts, in approximate order of similarity to this proposal:
bevyengine/bevy#9094— "TypePath required for generic#[reflect(ignore)]field" (open, re-opened byMrGVSV,A-Reflection). The strongest match. OP hits the exact same wall (leafwing-input-manager'sActionState<A: Actionlike>cannot be registered becauseA: TypePathis required even with#[reflect(ignore)]). Initial maintainer answer: "this cannot be lifted." Then re-opened byMrGVSVwith three options:#[type_path(unstable)]opt-out usingcore::any::type_name(≈ this proposal's option A / option D).#[type_path(ignore_params(T))]to omit a parameter from the path (≈ this proposal's option G).No upstream PR has landed any of these. The thread has been quiet since 2023. The proposal here is not a duplicate; it expands MrGVSV's option-space and adds the structural
bevy_reflect_traitssplit (option B) and the runtime-registry escape (option E), neither of which appear in #9094.bevyengine/bevy#19171— "Error registering generic type withPhantomData" (open, 2026,C-BugA-Reflection). Same root cause, narrower framing —register_type::<Foo<T>>()fails becauseT: TypePath + Send + Sync. No proposed fix; just a bug-report.bevyengine/bevy#18606— "Having more than two types param in a type and deriving Reflect errors" (closed, dup ofbodil/smartstring#7). Different problem (where-clause expansion bug), surfaced through the sameT: TypePathwall.bevyengine/bevy#5144/#5145— "PhantomDatadoes not implementReflect" (closedS-Wontfixthen partly addressed). Resolved at the level ofPhantomData<T>itself, not at the level ofT.bevyengine/bevy#9929— "implement Reflect forBox<T>" (open). Adjacent: foreign-type-as-field, not foreign-type-as-generic-parameter.bevyengine/bevy#5432— closed. AboutTypeUuid(predecessor ofTypePath) on primitives in generics; superseded by stable type path v2.bevyengine/bevy/discussions#2570— "Ergonomics of implementing Reflect for complex and external types." Discusses foreign-type-as-field; the resolution path was#[reflect_remote](PR#6042). Does not address foreign-type-as-generic-parameter.bevyengine/bevy/discussions#8349— "Reflection for External Crates." Same scope as#2570; recommendsbevy_mod_component_mirrorfor foreign types as fields.bevyengine/bevy#12691— "MakeTyped,TypePath,GetTypeRegistration, etc supertraits ofReflect" (open). Tangential — about whetherT: Reflectshould implyT: TypePath. Doesn't address the foreign-marker case.bevyengine/bevy#7184— "reflect: stable type path v2" (merged). The PR that introducedTypePathand the recursive composition that creates the orphan-rule pinch we hit. Establishes the design rationale: stable serialized scenes require stable strings, which is whycore::any::type_namewas rejected.The two issues this proposal originally cited (
#5146,#7775) are about stable-string generation generally, not the foreign-marker-as-generic-parameter case — that distinction holds up.Course correction recommended: the upstream-facing version of this proposal should explicitly open with a reference to
#9094and creditMrGVSV's three-option sketch, then position our additions (B = trait-only sub-crate; E = runtime registry; explicit framing as a class-of-bug rather than per-crate workaround) as the unhandled half of that conversation. Not a perfect duplicate — no need to pivot to a+1. But pretending #9094 doesn't exist will look like inadequate prior-art search. Add MrGVSV's option 3 (#[type_path(ignore_params(T))]) as a fourth consumer-side option (now option G above).Q2 — Does the
#[reflect(via_param = LocalShim<P>)]attribute (option C) break scene round-trip?Answer: yes, definitively. Switching a generic parameter from
EarthtoEarthShimmid-flight changes the parent struct's registeredtype_path()string, and that string is the registry key the scene deserializer looks up. Option C cannot be sold as a transparent ergonomic fix; it would have to be marked as a breaking format change.Evidence, all from
bevy_reflect0.18.1 in the local cargo cache:TypePath::type_path()literally concatenates each generic parameter'stype_path():bevy_reflect_derive-0.18.1/src/derive_data.rs:1273-1305—long_type_path()callsreduce_generics()which emits<#ident as TypePath>::type_path()for eachTypeParam, joined intomodule::Outer<param1, param2, ...>. Same pattern at lines 1310-1341 forshort_type_path(). SoTranslationalStateC<Earth>::type_path()is verbatim"crate::TranslationalStateC<earth_crate::Earth>"— changeEarthtoEarthShimand the parent path becomes"crate::TranslationalStateC<consumer_crate::EarthShim>".TypeRegistryis keyed by that exacttype_path()string for cross-type lookup:bevy_reflect-0.18.1/src/type_registry.rs:326—type_path_to_id.insert(registration.type_info().type_path(), registration.type_id()).type_path_to_id:bevy_reflect-0.18.1/src/serde/de/registrations.rs:7-12, 40-47— doc: "This deserializer expects a string containing the full [type path] of the type to find theTypeRegistrationof." Code:self.0.get_with_type_path(type_path). Thebevy_reflect::TypePathdoc itself reinforces this stability contract atbevy_reflect-0.18.1/src/type_path.rs:14-25("(de)serialization, rely on type paths as identifiers for matching dynamic values to concrete types").#[reflect_remote]attribute (bevy_reflect-0.18.1/src/remote.rs,bevy_reflect_derive-0.18.1/src/remote.rs:139-212) works at the field level: it generatesimpl ReflectRemote for LocalShimwith the shim'sTypePath, and the parent struct'stype_path()includes the shim's path because that's what its field is. There is no machinery for surfacing the underlying foreign type's path through a wrapper. Once a remote-shim is the field type, it is the registered type — and any scene written before the swap will key against the foreign type's path and fail to deserialize after the swap. Same algebra applies for the proposedvia_paramextension at the generic-parameter level.Implication for the proposal: option C is not a transparent ergonomic fix. It is a scene-format-breaking rename of every type using the wrapped parameter. It can still ship — but as an explicitly-breaking option targeting Bevy's next major release, with a migration story. The proposal already flagged this risk; this investigation upgrades it from "needs verification" to "confirmed breaking." Recommend marking option C
[breaking; major-version only]in the recommendation table.Q3 — Documented "parametric component → wildcard + discriminator" idiom?
Answer: no. There is no documented Bevy-blessed pattern for "I want generic component discrimination, but use a wildcard + sibling marker instead." The official documented workaround is option 3 from this proposal — the local newtype wrapper. Lifting our wildcard pattern into a Bevy idiom is therefore a real opportunity, but it is new documentation, not codification of an existing one.
Evidence:
https://bevy.org/learn/migration-guides/0-10-to-0-11/) prescribes a single workaround for the foreign-generic-parameter case, verbatim: "If you don't own a type you may need to wrap it in a newtype and manually implementTypePathfor the newtype." That is the local-newtype-wrapper option (this proposal's option 3 / a stripped-down option C). It is the only documented workaround.Component<T>patterns and marker components, but contain zero references to the wildcard-plus-discriminator idiom or to type-erasure as a workaround for generic-component reflection.tainted-coders.com/bevy/components— same: covers marker components andPhantomData<T>parametric components, no wildcard-plus-discriminator pattern.bevy_animation,bevy_asset,bevy_pbr,bevy_render,bevy_ecs). Bevy's own parametric components (Material<T>,AssetLoader<T>) requireT: TypePathand accept that downstream users lose anyTthat isn't owned-or-Reflect-able.bevy_mod_erased_component_registry) implements something near this idiom — type-erased components keyed by a siblingTypeId— but it is third-party, niche, and not referenced from any official Bevy doc.bevyengine/bevy#9094proposes the wrapper pattern as the fix and never mentions a wildcard-plus-discriminator alternative. None of the comment threads on#19171,#18606,#5144,#9094suggest it either.Implication for the proposal: the wildcard-plus-discriminator pattern this codebase ended up using (
<SelfPlanet>+IntegSourceC) is genuinely novel territory upstream. It is a viable alternative to option 3 (manual newtype wrappers) when the consumer would have to write one wrapper per marker — exactly our case (Earth, Mars, Moon, …). Lifting it to a documented idiom + helper macro (something like#[derive(WildcardComponent)] #[discriminator(IntegSourceC)]) would be a real contribution upstream, but it is additive new design, not codification of an existing convention. The original proposal's framing — "if Bevy already has this idiom, document it" — should be updated to "propose this idiom + helper as a new addition; it is currently unmapped."Q4 — Does
bevy_reflect_traitsextraction (option B) stand on its own?Answer: yes — strongly. Independent maintainer-driven work is already in motion to make
bevy_reflectoptional / decomposable, and at least one upstream reviewer has explicitly raised the question of separatingTypePathfrom the rest of reflection. The trait-only-sub-crate split lands directly in that ongoing conversation; foreign-marker support would be a side-effect, not the primary justification.Evidence:
bevyengine/bevy#20337— "Reflection should be fully optional and toggleable at abevylevel" (open, opened byalice-i-cecile, labelledS-Ready-For-Implementation,D-Modest,A-Cross-Cutting). Direct quote: "Reflection results in significant increases to compile times and binary size, and in many projects, is only useful as a development tool." The maintainer-prescribed shape: "bevy_reflectshould be an optional dependency for all Bevy crates whenever possible, and we should have areflectfeature flag onbevyitself that controls this. […] this will need to be mirrored tobevy_internal." That is the exact problem option B solves at the_traitslevel — except option B targets the trait definitions, while #20337 targets the runtime. The two are complementary, not competing.bevyengine/bevy#22721— "Make reflection optional" (open PR). Addsenable_codegenfeature tobevy_reflect_derivethat no-ops the proc macros. Reviewerashivaram23raised verbatim: "ShouldTypePathbe separated from this, since it is used independent of the rest of reflection over Bevy? Mainly for Asset like in#18501." This is the specific structural question option B answers. The PR is currentlyS-Needs-Review/X-Needs-SME, so the discussion is open.bevyengine/bevy#18501— closed, "bevy_image: deriveTypePathwhen Reflect is not available." Direct evidence that Bevy itself has abevy_*crate (bevy_image) that wantsTypePathwithout the rest ofbevy_reflect's runtime, and currently has to satisfy that with a feature-gated workaround. Trait-only sub-crate would be the structural fix.bevy_reflectitself:bevy_reflect_deriveis a separate proc-macro crate (paralleling thebevy_reflect_traitssplit this proposal proposes). The trait sigs are a tiny fraction of the crate by line-count:type_path.rs243 lines,reflect.rsis mostly trait sigs,from_reflect.rsis small. The bulk —TypeRegistry,serde/,func/,path/,impls/{glam,uuid,petgraph,wgpu_types,smallvec,indexmap,smol_str,foldhash,hashbrown}.rs,dynamic*types — is runtime / serialization / foreign-impls. The trait-only crate would be roughly the union oftype_path.rs,reflect.rs,reflectable.rs, the supertraits infrom_reflect.rs, plus the registration entry-point, with noserde, noimpls/, no dynamic types. Conservative estimate: ~10% ofbevy_reflect's current line-count and dependency footprint.bevy_image-style sub-crates (#18501) that wantAssetregistration viaTypePathwithout paying forbevy_reflectruntime.T: TypePathbounds even when reflection is feature-gated off. Right now those bounds force the heavy runtime to be present.bevy_egui-adjacent,bevy-inspector-egui-adjacent) that want to publishReflect-able types without committing every consumer to the full runtime.no_std/ WASM / size-constrained targets where theTypeRegistryandserdeweight is unwanted butT: TypePathbounds are still desired (the migration guide explicitly saysAssetrequiresTypePath).Implication for the proposal: option B is the strongest of the five and should be moved to the headline recommendation. It is already an open upstream conversation with explicit maintainer (alice-i-cecile) and reviewer (ashivaram23) interest. Foreign-marker support is one of several payoffs, not the load-bearing justification. The upstream-facing version of this proposal should reframe option B as "complete the optional-reflection refactor that #20337 / #22721 / #18501 are already pursuing, by extracting
bevy_reflect_traitsas the trait-only crate. As a side effect, this resolves the foreign-marker class of bug raised in #9094 / #19171." That framing converts the proposal from a niche request into the missing piece of an in-progress refactor.Bottom line on whether to take this upstream
#9094(foreign-marker / generic-parameter case) and#20337/#22721(optional-reflection / trait-extraction case). Both threads have maintainer activity and would absorb this proposal naturally.#9094proposing options A–E (crediting MrGVSV's three-option sketch and adding our two), and a parallel comment on#22721arguing for the trait-only-sub-crate split as the structural form of "make reflection optional." Reference each thread from the other.#[type_path(ignore_params(T))]from#9094as a fourth consumer-side option (option G in the structured option list above).References
simnaut/bevy_jeod#300— internal tracking issue this proposal addresses (promoteTranslationalStateCfrom<SelfPlanet>wildcard to<P: Planet>).simnaut/bevy_jeod#296,#315,#316,#304— wildcard-pattern design history; document the cost we are paying today.crates/jeod_quantities/src/frame.rs— thePlanettrait,define_planet!macro,SelfPlanet,PlanetInertial<P>definitions. Bevy-free by design.src/components.rs— every Bevy component pins its phantom at the wrapper boundary;TranslationalStateC(pub TranslationalStateTyped<PlanetInertial<SelfPlanet>>).CLAUDE.md§"Three-Layer Architecture" — thejeod_*(physics) /jeod_sim(orchestration) /bevy_jeod(Bevy wiring) layering rule.bevy_reflect/src/type_path.rs— theTypePathtrait + stability rationale.bevy_reflect/src/impls/— precedent for reflecting foreign crate types from insidebevy_reflect(glam,uuid,petgraph,wgpu_types); precisely the path that is not available to end-user crates.bevy_reflect::reflect_remote— solves the dual problem (foreign type as a field), does not help with foreign type as a generic parameter.Filed in
simnaut/bevy_jeodfirst for review. Upstream-facing draft comments forbevyengine/bevy#9094andbevyengine/bevy#22721are posted as comments on this issue.