graphql-hive / graphql-hive/federation-composition
support `@oneOf` directive
- Dominant language
- TypeScript
- Stars
- 53
- Forks
- 8
- Avg merge
- 20h 50m
- Merged PRs (30d)
- 10
Description
## Bug / Feature Request
`composeServices` from `@theguild/federation-composition` strips the standard GraphQL `@oneOf` directive (and other GraphQL-spec directives like `@specifiedBy`) during composition. Subgraphs that emit valid GraphQL with `@oneOf` on input types end up with a supergraph SDL where the directive is gone — both the definition and every application — leaving consumers with a plain `INPUT_OBJECT` that has lost its tagged-union semantics.
## Why this matters
`@oneOf` is part of the official GraphQL spec ([RFC merged Oct 2023](https://github.com/graphql/graphql-spec/pull/825)) and is the canonical way to model tagged-union inputs. It is not user-defined — every spec-conformant GraphQL implementation now supports it natively. Tools that emit GraphQL from other schema languages (e.g. OpenAPI loaders that map `oneOf` + `discriminator` to `@oneOf` input types) rely on the directive surviving composition so that downstream gateways can route discriminated input correctly.
Today, the only way to keep `@oneOf` alive through composition is to opt-in via Apollo Federation's `@composeDirective(name: "@oneOf")` mechanism, which itself requires:
1. Adding a matching `directive @oneOf on INPUT_OBJECT` definition to the subgraph SDL.
2. Adding a custom `@link(url: "...", import: ["@oneOf"])` to the schema definition.
3. Adding `@composeDirective(name: "@oneOf")` to the schema definition.
That's reasonable for genuinely user-defined directives. It is ceremonial overhead for a directive that the GraphQL spec already mandates.
## Where it gets stripped
`subgraph/state.js` ignores any directive application not in `composedDirectives`:
```js
// subgraph/state.js
Directive(node) {
if (composedDirectives.has(node.name.value)) {
// save directive into state
}
// else: silently dropped
}
```
`composedDirectives` is populated only by `compose-directive` validation when it sees `@composeDirective`:
```js
// subgraph/validation/rules/elements/compose-directive.js
context.stateBuilder.directive.setComposed(matchingDirective.name.value);
context.stateBuilder.composedDirectives.add(matchingDirective.name.value);
```
`compose.js` hardcodes the supergraph SDL header to only reference federation specs (`link`, `join`, `tag`, `inaccessible`, `policy`, `requiresScopes`, `authenticated`). There is no slot where `directive @oneOf on INPUT_OBJECT` would be added.
A grep across the whole package confirms zero references to `oneOf`. The composer has no concept of standard GraphQL directives beyond Federation's own.
## Repro
Minimal subgraph:
```graphql
schema { query: Query }
type Query { _empty: String }
directive @oneOf on INPUT_OBJECT
input CatVariant @oneOf {
small: SmallCat
big: BigCat
}
input SmallCat { weight: Int }
input BigCat { weight: Int, fierce: Boolean }
```
Run through `composeServices`. The supergraph SDL contains `input CatVariant { ... }` with no `@oneOf` directive applied and no `directive @oneOf` definition. A gateway that builds an executable schema from that SDL has no way to know `CatVariant` is a tagged union.
## Real-world impact
We hit this with `graphql-mesh` + `@omnigraph/openapi`. The OpenAPI loader correctly emits `@oneOf` on input types derived from `oneOf` schemas (see `getComposerFromJSONSchema.js`'s "oneOf without sibling properties" branch). The subgraph SDL is fine. But after `composeSubgraphs` (which delegates to this package), the directive is gone, and Mesh's runtime `resolveDataByUnionInputType` — which gates wrapper-key unwrapping on `getDirective(schema, type, 'oneOf')` — never fires. Wrapped input objects leak through to the REST upstream, where they get rejected.
We worked around it with a defensive unwrap shim on the upstream service. Functional, but the shim shouldn't have to exist; the directive marker is already in the subgraph and was deliberately dropped by the composer.
## Proposed fix
Auto-allowlist a small set of standard GraphQL directives that are part of the spec, no opt-in required:
- `@oneOf` (input tagged unions)
- `@specifiedBy` (scalar URL specifications)
- Possibly `@deferrable` / others as the spec evolves.
Sketch:
```js
// subgraph/state.js
const SPEC_DIRECTIVES = new Set(['oneOf', 'specifiedBy']);
// inside subgraph state setup:
for (const name of SPEC_DIRECTIVES) {
composedDirectives.add(name);
state.directives.setComposed(name);
}
```
…plus emitting the directive definitions in the supergraph SDL header when they are used (similar to `usedTagSpec` / `usedInaccessibleSpec` flags).
Alternative: detect `directive @oneOf on INPUT_OBJECT` in any subgraph SDL and treat it as auto-composed without requiring `@composeDirective`. The directive's location set is fixed by the spec, so no merge-conflict logic is needed.
## Environment
- `@theguild/federation-composition`: latest
- Composing OpenAPI-derived subgraphs via `@graphql-mesh/fusion-composition`
- Node 22
Happy to send a PR for `@oneOf` + `@specifiedBy` if the approach sounds reasonable.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.