Skip to content

[graphql] Add @typespec/graphql emitter#11000

Draft
FionaBronwen wants to merge 147 commits into
microsoft:mainfrom
pinterest:feature/graphql
Draft

[graphql] Add @typespec/graphql emitter#11000
FionaBronwen wants to merge 147 commits into
microsoft:mainfrom
pinterest:feature/graphql

Conversation

@FionaBronwen

Copy link
Copy Markdown
Contributor

Summary

This PR introduces @typespec/graphql, a new emitter that generates GraphQL SDL (Schema Definition Language) from TypeSpec definitions.

Features

  • Schema generation: Emit complete GraphQL schemas from TypeSpec models and operations
  • Operation types: Support for @query, @mutation, and @subscription decorators
  • Interface composition: Use @Interface and @compose for GraphQL interface inheritance
    patterns
  • Visibility filtering: Automatic input/output type generation based on @visibility decorators
  • Custom scalars: @specifiedBy decorator for custom scalar URL specifications
  • Schema customization: @schema decorator for multi-schema scenarios

Architecture

The emitter uses a two-phase approach:

  1. Mutation phase: Transforms TypeSpec types into GraphQL-compatible structures using the mutator framework, handling visibility filtering, naming conventions, and type deduplication
  2. Render phase: Uses @alloy-js/graphql components to emit the final SDL output

Example

import "@typespec/graphql";

using TypeSpec.GraphQL;

@query
op getUser(@path id: string): User;

@mutation
op createUser(input: User): User;

model User {
  id: string;
  name: string;
  email: string;
}

Generates:

  type Query {
    getUser(id: String!): User!
  }

  type Mutation {
    createUser(input: UserInput!): User!
  }

  type User {
    id: String!
    name: String!
    email: String!
  }

  input UserInput {
    name: String!
    email: String!
  }

Test plan

  • Unit tests for all decorators and components (318 tests passing)
  • E2E tests covering schema generation scenarios
  • Mutation engine tests for type transformation logic

Notes

timotheeguerin and others added 30 commits November 5, 2024 07:59
Add CODEOWNERS for graphql pull request reviews

---------

Co-authored-by: swatikumar <swatikumar@pinterest.com>
…#5033)

### Description

This PR sets up the flow to use the GraphQL emitter by providing an
interface for the various options that the GraphQL emitter will use
eventually. It also sets up test-hosts to work with these options. The
actual schema emitter doesn't really do anything other than emit "Hello
World" as it did previously, but the options get pass through as
confirmed by the test case.

Going forward, we can change the code in schema-emitter.ts to setup it
up for GraphQL using `navigateProgram`. We need to add diagnostics in
the emitter lib definition, but that can be done in a separate PR. The
next PR will have the outer layer of the GraphQL emitter setup to deal
with multiple schemas similar to multiple services in the OAI emitter.

### Testing
Run the tests and see that they pass.

---------

Co-authored-by: swatikumar <swatikumar@pinterest.com>
These are just some basic updates to the metadata in the
`@typespec/graphql` `package.json`. This brings it in line with other
packages like `@typespec/openapi3`.
The `@compose` decorator is used to indicate that a GraphQL `type` or `interface` implements one or more interfaces.

As [defined by the GraphQL spec](https://spec.graphql.org/October2021/#sec-Interfaces), a `type` or `interface` implementing an interface must contain all the properties defined by that interface, and so we require that the TypeSpec model implement the interface's properties as well. There is no restriction on how this is accomplished (via spread, via composition, via manual copying of properties, etc).

To reduce confusion, we do not allow a TypeSpec model to define both a `type` and an `interface`. In order to define an `interface`, the model must be decorated with the `@Interface` decorator.
The `@operationFields` decorator is used to specify one or more operations that should be placed onto a GraphQL type as fields with arguments.

This is our solution for representing [GraphQL field arguments](https://spec.graphql.org/October2021/#sec-Field-Arguments) in TypeSpec, as TypeSpec does not support arguments on model properties.
Implement `@Interface` and `@compose` decorators
Implement `@operationFields` decorator
Import useStateMap from compiler utils (after merge)
A few updates to make the project buildable again:
- `implements` became a reserved keyword in 9a4463b, so we switch to `interfaces`
- `expectIdenticalTypes` was renamed to `expectTypeEquals` in 32ca22f
- `validateDecoratorTarget` was removed in 32ca22f, as the TypeSpec type system handles this
- `SyntaxKind` was moved to `compiler/ast` in 32ca22f
- we also bump version of `devDependencies` to match those in other projects
Add main.tsp and remove strict from tspconfig.yml
Comment thread eng/common/pipelines/ci.yml Outdated
Comment thread .github/CODEOWNERS Outdated
Comment thread packages/graphql/lib/interface.tsp Outdated
Comment thread packages/graphql/lib/schema.tsp
Comment thread packages/mcp-server-typespec-docs/src/indexer.ts Outdated
Comment thread packages/graphql/lib/nullable.tsp
* - **Operation**: return type `T | null`
* - **Union**: named unions like `Cat | Dog | null` (safe — new unique object)
*/
export function isNullable(type: Type): boolean {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way those decorators are implemented kinda goes against the philosphy of those decorators(shouldn't really have to check the .decorators list unless there is a feature gap in the compiler)

Any reason to do that over using the stateMap/stateSet(and potentially as mentioned in another comment the upcomming auto decorators?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @nullable decorator is applied programmatically by the mutation engine after stripping | null variants from unions. (In GraphQL, unions don't have null variants, they just become null themselves.) We check .decorators rather than stateMap because we need the state to travel with cloned types. stateMap entries point to the original type objects and would be lost after cloning during mutation. Keeping this information alongside the cloned type via a decorator, enables mutation engine chaining where each mutation phase can work on the transformed output of the previous one and still contain the nullability context.

Since auto decorators #10197 use stateMap I think we'd run into the same issue. Happy to discuss if there's a better pattern that still allows us to chain together mutation stages without auxiliary data to pass around!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hhm so I think this expose a different issue then, when you clone a type it should clone the decorators and run them and in turn updating the state map.

My guess here is that in the mutation you are not actually calling finishType on the type.(You need to do that to make sure the decorators run) It has been a common mistake when writing emitters

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a closer look at finishType, and I want to clarify our use case since it's a bit different from the scenario you're describing.

The mutator-framework does call finishType on cloned types, so I don't think that's the issue.

The key difference is that we're not trying to preserve stateMap entries from the original type. We're programmatically injecting metadata during mutation. The flow is like this:

  1. User writes a union T | null in TSP (no @nullable decorator in source TSP)
  2. Mutation engine unwraps the union to T and calls setNullable(type) to mark it
  3. Downstream mutation engines/emitters check isNullable(type) to emit the correct nullability

Since the decorator wasn't in the original source, there's nothing for finishType to re-run. We're using the decorators array as a portable metadata container — the decorator function reference ($nullable) is just a unique identifier, and the function itself is intentionally a no-op.

This pattern gives us a simple contract for multi-stage mutation pipelines: metadata travels with the type object itself, no shared stateMap infrastructure needed between stages.

I know this isn't perhaps the intended decorator usage, but it seemed like the cleanest way to transport information between mutation engines without auxiliary data structures. If there is a different mechanism for this, please let me know!

Comment thread packages/graphql/src/lib.ts Outdated
Comment thread packages/graphql/src/index.ts
Comment thread packages/graphql/src/lib.ts
Comment thread .gitignore
- Revert ci.yml change that added feature/* to PR triggers
- Add documentation to SchemaOptions model and name property
- Add clarifying comment for regex in type-utils.ts
  (addresses false-positive HTML injection warning from security bot)
Remove mcp-server-typespec-docs package and .mcp.json config file
as they are unrelated to the GraphQL emitter.
Replace the local alloy/packages/graphql dependency with the new
@pinterest/alloy-graphql package. Update @alloy-js/core to 0.23.1
in the workspace catalog to match alloy-graphql's peer dependency.
TypeSpec uses backtick escaping for decorators that conflict with
reserved keywords (e.g., @`package`, @`scenario`). The @interface
decorator was incorrectly using PascalCase to avoid the `interface`
keyword conflict.

This change renames @interface to @`interface` to follow the
established TypeSpec convention for keyword-escaping.

Changes:
- lib/interface.tsp: extern dec `interface`
- src/lib/interface.ts: $interface
- src/tsp-index.ts: interface: $interface
- Updated all tests, docs, and error messages
Remove NAMESPACE export from lib.ts and individual namespace exports from
decorator files. Register all decorators via the $decorators export in
tsp-index.ts. Update .tsp files to import from tsp-index.js instead of
individual decorator JS files.
Demonstrates queries, mutations, object types, enums, and nullable returns
for the @typespec/graphql emitter.
@microsoft-github-policy-service microsoft-github-policy-service Bot added the meta:website TypeSpec.io updates label Jun 23, 2026
- Add guide.md with usage documentation for the GraphQL emitter
- Generate reference docs using tspd doc tool
- Add GraphQL to sidebar under Emitters section with preview badge
- Add regen-docs script and tspd dev dependency to graphql package
External contributors cannot be codeowners in the Microsoft org.
Allows running validation without writing output files when using --dry-run flag.
Implement $onValidate to report the empty-schema diagnostic before
the emitter runs, providing immediate IDE feedback when a schema
has no @query, @mutation, or @subscription operations.

- Only validates namespaces with @Schema decorator
- Removes duplicate diagnostic from emitter.tsx (keeps skip-generation)
- Adds @query operations to test namespaces to avoid spurious warnings
Add early validations per GraphQL spec:
- empty-enum: Enums must define at least one value
- empty-union: Unions must have at least one non-null member
- reserved-name: Names must not begin with "__" (reserved for introspection)

The reserved-name check validates:
- Type names (models, enums, unions)
- Field/property names
- Operation parameter names
- Enum member names

The empty-union check catches unions that will be empty after null-stripping
(e.g., `union Foo { none: null }`) before the mutation engine runs.

Spec references:
- https://spec.graphql.org/September2025/#sec-Enums
- https://spec.graphql.org/September2025/#sec-Unions
- https://spec.graphql.org/September2025/#sec-Names.Reserved-Names
@timotheeguerin

Copy link
Copy Markdown
Member

/azp run typespec - pr tools

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@pkg-pr-new

pkg-pr-new Bot commented Jun 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/microsoft/typespec/@typespec/graphql@11000

commit: 954e220

@github-actions

Copy link
Copy Markdown
Contributor

❌ There is undocummented changes. Run chronus add to add a changeset or click here.

The following packages have changes but are not documented.

  • @typespec/graphql
Show changes

Comment thread packages/graphql/package.json Outdated
Comment thread packages/graphql/test/lib/template-composition.test.ts Outdated
Comment thread packages/graphql/lib/interface.tsp Outdated
* }
* ```
*/
extern dec `interface`(target: Model, options?: valueof {interfaceOnly?: boolean});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how common is this decorator going to be used? It is a bit unfortunate that we'll have to escape all usages, i see why it was called Interface, other options maybe

@iface

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interfaces are a core feature that I would expect to come up a fair amount. So yeah, totally agree about the escaping. Maybe @graphqlInterface is better? Updated to try that out.

Comment thread packages/graphql/test/emitter.test.ts Outdated
Comment thread packages/graphql/test/mutation-engine/schema-mutator.test.ts Outdated
The backtick-escaped `@\`interface\`` syntax was awkward to use. Rename to
@graphqlInterface which is clear, follows camelCase convention used by other
multi-word decorators (@bodyRoot, @statuscode, @httpFile), and doesn't require
escaping.
- Replace verbose Awaited<ReturnType<typeof Tester.createInstance>> with
  TesterInstance type across all test files
- Simplify template-composition.test.ts to call Tester.compile() directly
  instead of using beforeEach/createInstance pattern (this test intentionally
  uses its own Tester with libraries: [] since it tests a pure utility function)
- Remove unnecessary t.model/t.op/t.enum/t.union wrappers when not
  extracting types from compile result
- Extract Cat directly from compile() in decorator args corruption test
- Use plain string templates instead of t.code where extraction not needed
When tests don't extract types from compile() but instead lookup from
namespace, they don't need t.model/t.op/t.enum/t.union wrappers.
Simplified these tests to use plain string templates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:graphql Issues for @typespec/graphql emitter emitter-framework Issues for the emitter framework eng meta:website TypeSpec.io updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants