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
TypeSpec is a protocol agnostic language. It could be used with many different protocols independently or together
TypeSpec's standard library includes support for emitting OpenAPI 3.0, JSON Schema 2020-12 and Protobuf.
As GraphQL is a widely used protocol for querying data by client applications, providing GraphQL support in the TypeSpec standard library can help bring all valuable TypeSpec features to the GraphQL ecosystem.
This proposal describes the design for a GraphQL emitter that can be added to TypeSpec's standard library and can be used to emit a valid GraphQL schema from a valid TypeSpec definition.
All types within a GraphQL schema must have unique names. No two provided types may have the same name. No provided type may have a name which conflicts with any built in types (including Scalar and Introspection types).
All anonymous types in TSP will need a default name (something like namespace + parent_type + field_name). If a type in TSP results in multiple types in the output, each output type should be unique by having a prefix or suffix (something like type + “Interface”)
All types and directives defined within a schema must not have a name which begins with “__” (two underscores), as this is used exclusively by GraphQL’s introspection system. GraphQL Identifiers have this validation (names can only start with an _ or letter)
The query root operation type must be provided and must be an Object type.
If the resulting GraphQL schema has no query type, create a dummy query type with no fields
Custom scalars should provide a @specifiedBy directive or the specifiedByURL introspection field that must link to a human readable specification of data format, serialization, and coercion rules for the scalar
Use existing open source specification for custom scalars that already provide these details and use the @specifiedBy directive in the schema to point to them
Object Types and Input Types are two completely different types in GraphQL
See object type and input types for more details
An object type must define one or more fields
Throw an error if we encounter an empty object
Basic emitter building blocks
The following building blocks are used by the emitter code:
emitter.ts (starting point)
Responsibilities:
- Resolving emitter options like noEmit, strictEmit, ...
- Create the actual gql-emitter with the output filePath and options
gql-emitter.ts (main file)
Creates a GraphQLEmitter class that initializes the registry, and typeSelector
- Starts navigateProgram that collects all the types and builds the GraphQL AST
- Creates the top-level query/mutation/subscriptions
- Creates a new GraphQLSchema (js object)
- Validates schema
- Returns schema if no errors
If not error
- printSchema(schema) (GraphQL method that handles all the formatting etc)
- Write string to file
registry.ts (several maps to collect types)
Mostly has 2 types methods:
- addXXX (addGraphQLType)
- getXXX (getGraphQLType)
The add methods add the partial type to a collector and the get methods are called in the exit visitors to finish building the type as all the information is available to do so.
selector.ts (exposes the function to select the right GraphQL type based on TSP type)
- typeSelector(type: Type): GraphQLOutputType | GraphQLInputType
The main design constraint is that we only want to traverse the TSP program once to collect and build the GraphQL types.
Detailed Emitter Design
Design Scenarios
We need to consider two main scenarios when designing the GraphQL emitter:
When the TypeSpec code is specifically designed for emitting GraphQL, we can equip developers with GraphQL-specific decorators, and objects. This will aid in crafting TypeSpec code that generates well-designed GraphQL schemas. Given that GraphQL does not employ HTTP or REST concepts, developers should be able to bypass those libraries. However, it should still be feasible to emit OpenAPI or any other schema by adding the appropriate decorators (like @route) to the existing TypeSpec code used to generate the GraphQL schema and the existing graphql emitter should continue to work as expected.
When a developer aims to create a GraphQL service from an existing TypeSpec schema originally used for emitters like OpenAPI, we focus on producing a GraphQL schema that represents the TypeSpec with no loss of specificity. Instead of assuming intent, we will provide errors and warnings as soon as possible when something in the TypeSpec schema is not directly compatible with GraphQL and the means of making it compatible are not deterministic.
Output Types
Context and design challenges
GraphQL distinguishes between Input and Output Types. While there is no way in TypeSpec to allow the developers to specify this, the compiler provides a mechanism that identifies each model as Input and/or Output using UsageFlags.
In GraphQL:
Scalar and Enum types can be used as both: Input and Output
Object, Interface and Union types can be used only as Output
Input Object types can't be used as Output
Design Proposal
Use the UsageFlags to identify the input and output types for GraphQL.
🔴 Design Decision: As TSP will allow a model to be both input and output type and indeed that would be useful for GraphQL as well, the GraphQL emitter will support this case. In order to differentiate between the input and output types we propose creating a new GraphQL type for inputs with the name of the type + Input suffix.
When creating an operation that returns models, all directly or indirectly referenced models, should be emitted as valid GraphQL output types.
# This is invalid
input Example {
self: Example!
name: String
}
# This is also invalid
input First {
second: Second!
name: String
}
input Second {
first: First!
value: String
}
For an optional input type, a null value can be provided, and that would be assigned to this type. Optional input types can also be “missing” from the input map. Null and missing are treated differently.
Design Proposal
To emit a valid GraphQL and still represent the schema defined in TypeSpec, the emitter will follow these rules:
If the Input model is Scalar or Enum, the type is generated normally.
If the input type is a Model and all the properties of the Model are of valid Input types, a new Input object will be created in GraphQL, with the typename as the original type + Input suffix.
🔴 Design decision: All models are created with the Input suffix regardless of whether or not it is used as both, because the model can be used as both input and output in the future and changing the type name will cause issues with schema evolution.
Cons: the Input suffix can be annoying or result in types like UserInputInput
If the model or its properties are invalid Input types, an error will be raised.
🔴 Design decision: In order to provide a different definition of the same field so that the GraphQL type can be represented more accurately, we will use visibility, see the examples to see what that could look like.
If the model contains an unbroken chain of non-null singular fields, throw an error and fail the emitter process
Mapping
TypeSpec
GraphQL
Notes
Model.name
Object.name
See Naming conventions
Model.properties
Object.fields
Examples
TypeSpec
GraphQL
/** Valid Input Model */modelUserData {
name:string;
email?:string;
age:int | null;
}
/** created user */modelUser {
...UserDataid: int32;
}
@mutationopcreateUser(userData:UserData):User
Create a new decorator to allow the TSP entities to belong to different protocols. This would be part of the TSP library similar to invisible and visible
Use this new way to define protocol specific entities
Auto-resolve unwrapping of unions
Even with the @invisible decorator applied to union variants, the emitter creators will have to deal with the auto-unwrapping of unions with just one variant. As this would be common functionality to all emitters, perhaps this should be done in a common place like by the TSP compiler
Scalars
Context and design challenges
GraphQL only provides five built-in scalars: Int, String, Float, Boolean and ID.
Any other scalar should be added as a custom scalar, and the @specifiedBy directive should be added to provide a specification.
The ID scalar type represents an unique identifier, as defined here.
Design Proposal
The emitter will use the mappings provided below to map TypeSpec to GraphQL scalars, trying to emit as a built-in scalar when possible.
For the custom scalars, if the TypeSpec documentation mentions a specification, that will be used for the @specifiedBy directive. If not provided, we will use a link to the TypeSpec documentation: https://typespec.io/docs/standard-library/built-in-data-types/
Encodings provided by the @encode decorator in TSP code would also be considered to build the proper custom scalar.
We are proposing a new TypeSpec native decorator @specifiedBy over scalars to allow developers to provide their own references. If provided, the emitter will use the information to generate the GraphQL directive.
To handle the ID type, the emitters library will include a TypeSpec scalar:
In GraphQL, all Unions should be named, while in TypeSpec anonymous Unions can be used.
Scalars, Interfaces and Unions can't be member types of an Union. Therefore, in GraphQL nested Unions are not permitted.
Unions can't be part of a GraphQL Input Object.
Design Proposal
Generate 1:1 mapping for regular unions.
For nested unions, a single union will be recursively composed with all the variants implicitly defined in TypeSpec.
As the interface models are decorated with an @Interface decorator, throw a validation error when defining a union variant for a model type that is decorated with this.
Wrap the scalars in a wrapping object type and emit a union with those types.
Create explicit unions in GraphQL for anonymous TSP unions, naming them using the context where the Union is declared, for example using model and property names, or the operation and parameter names, or the operation name if used as a return type. And all cases with the "Union" suffix. (See examples). Note that this approach may generate identical GraphQL unions with distinct names. We will throw an error if there are naming conflicts.
There are some special cases with distinct treatments, like:
TypeSpec does not support arguments on model properties.
Design Proposal
Create a new decorator called operationFields that references operations or interfaces to be added to a model
This will be used by the emitter to generate a field with arguments on the corresponding GraphQL type
Operations and namespaces that are used in the operationFields decorator are not emitted as part of the root GraphQL operations like query, mutation, or subscription
[DISCARDED] @parameters({arg1: type1; arg2: type2;}) decorator targeting Model Properties.
We prototyped this, but found issues when validating/generating the Input types.
[DISCARDED] @mapArguments(modelProperty, arg1, agr2, …) decorator over Operations, where arg1, arg2, etc. are the name of the parameters of the target operation to map as arguments of the modelProperty.
[DISCARDED] @modelRoute(model) decorator over Operations, where the model is passed as a parameter to the decorator. This would be used to map the operation as a new parameterized field of a model.
Interfaces
Context and design challenges
There is no way to represent GraphQL Interfaces in TSP directly. We’ll use a combination of special decorators and the spread operator to achieve this for the GraphQL emitter.
Only Output Types can be decorated as an Interface. If an Input Type is decorated as an Interface, a decorator validation error must be thrown.
Design Proposal
GraphQL Interfaces will be defined using the two specific decorators outlined below:
The @Interface decorator will designate the TSP model to be used as an Interface in GraphQL. This model will be emitted as the GraphQLInterface type.
The @compose decorator designates which Interfaces should the current model be composed of. The @compose decorator can only refer to other models that are marked with the @Interface decorator and not vanilla model types.
Mapping
TypeSpec
GraphQL
Notes
@Interface
interface
Model
interface (Output Type)
Note only output models can be interfaces
@compose
extends Iface1, Iface2…
@compose can be used either with a combination of the @interface decorator or on the model directly
Decorators
Decorator
Target
Parameters
Validations
@Interface
Model
Can be assigned only to an output model
@compose
Model
Targets of the Interface decorator
Can be assigned only to an output model All the fields of the models from compose must be present in the target model
Examples
TypeSpec
GraphQL
aliasID=string@InterfacemodelNode {
id:ID;
}
@Interface@compose(Node)
modelPerson {
id:ID; // This is from Node...Identity// This is just for TSP spread
}
modelIdentity {
birthDate:plainDate;
age?:integer;
}
@compose(Person)
modelActor {
...Personrating: string;
}
Fields within the composed model can be defined using either ... operator or manually, both are valid
GraphQL requires both Person and Node to be explicitly implemented by Actor.
Design Alternatives
[Discarded] Spread the fields of models defined in compose automatically – this wouldn’t be great because then compose would change the shape of the model just for GraphQL
[Discarded] Don’t define the Interface and assume interfaces from models used in compose. Since GraphQL has an explicit concept of Interface we’re representing that using this decorator. If validation rules specific to Interfaces need to be applied in the future, it will be possible to do so
Enums
Warning
This section is under review and possible reconsideration.
Context and design challenges
TSP enum member types have no meaning in GraphQL and the enum member values should follow the naming convention shown below (similar to all other literal names). From the GraphQL spec: “EnumValue Namebut not true false null”
where Name should start with [A-Za-z] or <underscore> and can be followed by letter, digit, or <underscore>
GraphQL Recommendation: “It is recommended that Enum values be “all caps”. Enum values are only used in contexts where the precise enumeration type is known. Therefore it’s not necessary to supply an enumeration type name in the literal.”
Design Proposal
Use TypeSpec enums in the value context as GraphQL doesn’t need the type information.
TypeSpec enums with no types that can only be identifiers or string literals will be translated to all caps GraphQL enums as long as the identifiers are valid GraphQL names. If they are invalid, the emitter will throw a validation error.
🔴 Design decision: TypeSpec enums with integer or floating point values will be converted to a string value using the following rules to create result:
Initialize result to _
If the integer is negative add the word NEGATIVE_ to the result string
Create a string representation of the integer or create a string representation of the floating point value where . is converted to an _
Append the string representation to result
Pros: The GraphQL enum is a string representation of the value and reflects the true intention of the developer
Cons: The server side implementation will have to figure out the translation between the GraphQL enum and the internal representation of the enum where the algorithm isn’t obvious (i.e. they will basically have to implement the steps above).
Inline enums that don’t have an enum name will be assigned a distinct name based on where the field appears in the TSP schema. The name derived from the field will be followed by an Enum suffix. To provide disambiguation, the full name should be namespace + modelName + fieldName. See the examples table for an example.
Derive a unique name based on the namespace, model, field name \+ “Enum”
enumDemoServicePersonSizeEnum {
SMALL MEDIUM LARGE
}
Design Alternatives
Use the type name instead of values for integer and floating point values. But, we would need to be consistent and use TSP enums in the type context rather than the value context which feels wrong.
Emit Any for enums with values as integers or floating points and let the developer define an alternate type using visibility.
If the @invisible decorator can be applied to EnumMembers, we can provide alternate enum members for GraphQL in the same enum definition which change the emitter to emit the GraphQL enum values as shown below:
There are three kinds of GraphQL Operations: Query, Mutation and Subscription. While in TypeSpec there is no difference between them.
At least one query operation should be included in the schema.
The models directly or indirectly used in the operation parameters should be declared as Input types
The models directly or indirectly used as the operation result type should be declared as Output types
Design Proposal
Warning
This section is under review. The means of associating operations with a given operation type may change.
To distinguish between Queries, Mutations and Subscription, we are proposing to include a set of three decorators in TypeSpec: @query, @mutation and @subscription. These will decorate the TSP Operations to indicate the GraphQL kind.
The decorators would also be added to an interface, understanding that all operations within the interface would be of the provided kind.
The GraphQL emitter will generate the proper GraphQL kind for each Operation, according to these rules:
Follow the explicit definition of the decorator: @query, @mutation, @subscription
If the decorator is not provided, then the operation would be omitted from the GraphQL schema
The Operation parameters will be converted to GraphQL arguments following the rules for the GraphQL Input types.
The Operation return type should be a valid GraphQL Output Type.
In line with the Field Arguments design, the operations decorated directly or indirectly with the @operationFields decorator, would not be added as query, mutations or subscriptions.
When no operation is emitted, an empty schema will be generated.
When mutations are provided, but there are no query operations, a dummy Query will be added to the schema to make it valid.
TSP defines a list and Array builtin types and both of those need to be converted to GraphQL lists. GraphQL lists are wrappers over output and input types.
Design Proposal
For TSP lists ([]) and arrays (Array) used as types of properties, parameters and operations, we will emit the corresponding list of types in GraphQL.
Mapping
TypeSpec
GraphQL
Notes
List.type
List.type
Array.type
List.type
Examples
TypeSpec
GraphQL
/** Lists as property types */modelUser {
id:int32;
pronouns:string[];
groups:Group[];
}
/** Lists as op return types */opgetUserAddresses(
id:int32;
):User[];
modelPet {
id:int32;
names:Array<string>;
}
Note the difference in the requiredness of the values vs the list itself for the various options
Nullable vs Optional
Warning
This section is under review. The approach described here will be overhauled if our Contextual Requiredness proposal is accepted.
Context and design challenges
In GraphQL, all properties and parameters are nullable by default, and the ! operator is applied to indicate non-nullability.
And although all fields are optional; for parameters, Input fields are required if they are marked as non-nullable.
In TypeSpec non-nullable is the default, while nullability is expressed by an Union that includes the null type. Also in TypeSpec: all the fields are required, unless are marked optional with the ? operator.
Design Proposal
All output types and return types will be emitted in GraphQL as non-nullable (! operator), except when the field is marked as optional, or when the type of the field is an Union containing the TypeSpec null type.
We can also use the same rules for Input fields, but we will force the field as required if the property or the argument is not nullable. Alternatively, we can throw an error.
[DISCARDED] Ignore TSP Optional operator and use only nullability.
Throw an error for Input types when they are nullable and not optional.
Visibility & Never
Context and design challenges
TypeSpec have two ways to filter out properties from Models:
Visibility, using @visibilty, @invisible, @withVisibility, et al decorators.
never type
The filtering based on explicit filtered models using @withVisibility is already considered in the compiler, so it will be also included in the emitter.
HTTP library has the automatic visibility concept that automatically filters the properties from the model based on the HTTP type of the operation, with no need of generating explicit filtered models.
According to the note in the TypeSpec documentation, it is the responsibility of the emitters to exclude the fields of type never.
Design Proposal
Add to the emitter the handling of the never type, and exclude any field from the Model before emitting the Model.
Note: This may result in empty models. We need to define what to do with fields pointing to empty Models.
For implicit filtered models (automatic visibility):
GraphQL does not have an equivalent concept like HTTP verbs that map to the Lifecycle visibility modifiers. However, GraphQL mutations will commonly adhere to these type of "CRUD" operations.
TSP developers will need to take advantage of the @parameterVisibility and @returnTypeVisibility decorators to filter the models based on the semantic operation type.
In the case where the operation does not have explicit visibility specified and is already decorated with an HTTP verb, the emitter will use the HTTP library specification to apply the related visibility to the input types.
If none of the standard "CRUD" operations apply, whether the operation is a query, mutation, or subscription will apply the OperationType.Query, OperationType.Mutation, or OperationType.Subscription visibility to input types, respectively.
For practical reasons, we will follow lead of the HTTP library on response types and filter them to Lifecycle.Read by default.
Generated model names will be suffixed with the appropriate operation type, e.g. UserQueryInput, UserRead, UserCreateInput, UserMutationInput, etc.
The new models would be generated only if they are distinct from the original Model.
""" postState is Int """typePost {
id: Int!title: String!isPopular: Boolean!poster: PersonpostState: Int!postCountry: Country
}
""" No postState is present due to never """typePostGql {
id: Int!title: String!isPopular: Boolean!poster: PersonpostCountry: Country
}
""" No poster because the visibility is read """typePostRead {
id: Int!title: String!isPopular: Boolean!postState: Int!postCountry: Country
}
Define what to do with fields pointing to empty models
Should we keep the original Models in the schema, even if they are not used?
We should expect that <Model>Read types will be the most common; should we have the Lifecycle.Read-filtered model instead be called <Model>, and the unfiltered model be something like <Model>Full?
User feedback:
The emitter will generate feedback for the developers through errors and warnings. But the warning list could be enormous and not easy to read, especially when trying to emit a GraphQL from a large TSP specification not specifically designed for GraphQL.
With this in mind we are proposing to emit a "How to improve your TypeSpec scheme for GraphQL" report based on the warnings and other signals. The purpose is to help developers to generate a better GraphQL schema, introducing the GraphQL decorators and other tricks to their TypeSpec code. The report should be more readable than the warnings.
GraphQL Emitter Design
Authors: @AngelEVargas, @swatkatz, @steverice
Last updated: Feb 13, 2025
Motivation
From the TypeSpec docs:
TypeSpec's standard library includes support for emitting OpenAPI 3.0, JSON Schema 2020-12 and Protobuf.
As GraphQL is a widely used protocol for querying data by client applications, providing GraphQL support in the TypeSpec standard library can help bring all valuable TypeSpec features to the GraphQL ecosystem.
This proposal describes the design for a GraphQL emitter that can be added to TypeSpec's standard library and can be used to emit a valid GraphQL schema from a valid TypeSpec definition.
General Emitter Design Guidelines
Refer to 4604
GraphQL spec and validation rules
_orletter)invisibilitydecorator to define another field with a GraphQL valid name@specifiedBydirective or thespecifiedByURLintrospection field that must link to a human readable specification of data format, serialization, and coercion rules for the scalar@specifiedBydirective in the schema to point to themBasic emitter building blocks
The following building blocks are used by the emitter code:
The main design constraint is that we only want to traverse the TSP program once to collect and build the GraphQL types.
Detailed Emitter Design
Design Scenarios
We need to consider two main scenarios when designing the GraphQL emitter:
@route) to the existing TypeSpec code used to generate the GraphQL schema and the existing graphql emitter should continue to work as expected.Output Types
Context and design challenges
GraphQL distinguishes between Input and Output Types. While there is no way in TypeSpec to allow the developers to specify this, the compiler provides a mechanism that identifies each model as Input and/or Output using
UsageFlags.In GraphQL:
ScalarandEnumtypes can be used as both: Input and OutputObject,InterfaceandUniontypes can be used only as OutputInput Objecttypes can't be used as OutputDesign Proposal
Use the
UsageFlagsto identify theinputandoutputtypes for GraphQL.🔴 Design Decision: As TSP will allow a model to be both
inputandoutputtype and indeed that would be useful for GraphQL as well, the GraphQL emitter will support this case. In order to differentiate between theinputandoutputtypes we propose creating a new GraphQL type for inputs with the name of the type +Inputsuffix.When creating an operation that returns models, all directly or indirectly referenced models, should be emitted as valid GraphQL output types.
Mapping
Model.nameObject.nameModel.propertiesObject.fieldsExamples
This results in an error
This results in an error
Based on result coercion rules if
urlis non-null, thennullorundefinedwill raise an error. So, we need to markurlas not required in GraphQL.More complicated examples with unions, interfaces, and lists are described in their respective sections.
Input Types
Context and design challenges
Use the
UsageFlags.INPUTto determine if a TSPmodelis aninputtype. The following validation rules apply toinputtypes:inputtype.Inputtypes may not be defined as an unbroken chain of Non-Null singular fields as shown below# This is invalid input Example { self: Example! name: String } # This is also invalid input First { second: Second! name: String } input Second { first: First! value: String }optionalinput type, anullvalue can be provided, and that would be assigned to this type.Optionalinput types can also be “missing” from the input map.Nullandmissingare treated differently.Design Proposal
To emit a valid GraphQL and still represent the schema defined in TypeSpec, the emitter will follow these rules:
ScalarorEnum, the type is generated normally.Modeland all the properties of theModelare of valid Input types, a newInputobject will be created in GraphQL, with the typename as the original type +Inputsuffix.Inputsuffix regardless of whether or not it is used as both, because the model can be used as bothinputandoutputin the future and changing the type name will cause issues with schema evolution.Inputsuffix can be annoying or result in types likeUserInputInputmodelor its properties are invalid Input types, an error will be raised.modelcontains an unbroken chain of non-null singular fields, throw an error and fail the emitter processMapping
Model.nameObject.nameModel.propertiesObject.fieldsExamples
This results in an error
Throw an error in emitter validation
Design Alternatives
For specifying GraphQL/HTTP specific types:
invisibleandvisibleAuto-resolve unwrapping of unions
@invisibledecorator applied tounion variants, the emitter creators will have to deal with the auto-unwrapping of unions with just one variant. As this would be common functionality to all emitters, perhaps this should be done in a common place like by the TSP compilerScalars
Context and design challenges
GraphQL only provides five built-in scalars: Int, String, Float, Boolean and ID.
Any other scalar should be added as a custom scalar, and the @specifiedBy directive should be added to provide a specification.
The ID scalar type represents an unique identifier, as defined here.
Design Proposal
The emitter will use the mappings provided below to map TypeSpec to GraphQL scalars, trying to emit as a built-in scalar when possible.
For the custom scalars, if the TypeSpec documentation mentions a specification, that will be used for the @specifiedBy directive. If not provided, we will use a link to the TypeSpec documentation: https://typespec.io/docs/standard-library/built-in-data-types/
Encodings provided by the @encode decorator in TSP code would also be considered to build the proper custom scalar.
We are proposing a new TypeSpec native decorator @specifiedBy over scalars to allow developers to provide their own references. If provided, the emitter will use the information to generate the GraphQL directive.
To handle the ID type, the emitters library will include a TypeSpec scalar:
Type Mappings to GraphQL Built-In Scalars
stringStringbooleanBooleanint32int16int8safeintuint32uint16uint8Intfloatfloat32float64FloatType Mappings to GraphQL custom Scalars
integerint64scalar BigIntStringnumericscalar NumericStringdecimaldecimal128scalar BigDecimalStringbytesbase64scalar BytesStringbase64urlscalar BytesUrlStringutcDateTimerfc3339scalar UTCDateTimeStringrfc7231scalar UTCDateTimeHumanStringunixTimestampscalar UTCDateTimeUnixIntoffsetDateTimerfc3339scalar OffsetDateTimeStringrfc7231scalar OffsetDateTimeHumanStringunixTimestampscalar OffsetDateTimeUnixIntunixTimestamp32scalar OffsetDateTimeUnixIntdurationISO8601scalar DurationStringsecondsscalar DurationSecondsIntorFloat, based on@encodeplainDatescalar PlainDateStringplainTimescalar PlainTimeStringurlscalar URLStringunknownscalar UnknownStringExamples
Unions
Context and design challenges
Design Proposal
Generate 1:1 mapping for regular unions.
For nested unions, a single union will be recursively composed with all the variants implicitly defined in TypeSpec.
As the
interfacemodels are decorated with an@Interfacedecorator, throw a validation error when defining aunionvariant for a model type that is decorated with this.Wrap the scalars in a wrapping object type and emit a union with those types.
Create explicit unions in GraphQL for anonymous TSP unions, naming them using the context where the Union is declared, for example using model and property names, or the operation and parameter names, or the operation name if used as a return type. And all cases with the "Union" suffix. (See examples). Note that this approach may generate identical GraphQL unions with distinct names. We will throw an error if there are naming conflicts.
There are some special cases with distinct treatments, like:
nulltype: see NullabilityMapping
Union.nameUnion.name• ModelPropertyUnion
• OperationParameterUnion
• OperationUnion
Union.typesUnion.typesExamples
Nested unions
Anonymous union in param
Named union of scalars
Named union of scalars and models
Anonymous union in return type
Design Alternatives
Union of scalars design alternative:
Anytype.AnytypesOpen Questions
Field Arguments
Context and design challenges
Design Proposal
operationFieldsthat referencesoperationsorinterfacesto be added to a modeloperationFieldsdecorator are not emitted as part of the root GraphQL operations likequery,mutation, orsubscriptionMapping
@operationFieldsModelOperation.nameField.nameOperation.returnTypeField.typeOperation.parametersField.ArgumentsMapDecorators
@operationFieldsModel@useAsQueryModelExamples
Additional examples that show namespaces in GraphQL can be found here:
Design Alternatives
@parameters({arg1: type1; arg2: type2;})decorator targeting Model Properties.We prototyped this, but found issues when validating/generating the Input types.
@mapArguments(modelProperty, arg1, agr2, …)decorator over Operations, where arg1, arg2, etc. are the name of the parameters of the target operation to map as arguments of the modelProperty.@modelRoute(model)decorator over Operations, where the model is passed as a parameter to the decorator. This would be used to map the operation as a new parameterized field of a model.Interfaces
Context and design challenges
There is no way to represent GraphQL Interfaces in TSP directly. We’ll use a combination of special decorators and the spread operator to achieve this for the GraphQL emitter.
Only Output Types can be decorated as an
Interface. If anInput Typeis decorated as anInterface, a decorator validation error must be thrown.Design Proposal
GraphQL Interfaces will be defined using the two specific decorators outlined below:
The
@Interfacedecorator will designate the TSP model to be used as an Interface in GraphQL. This model will be emitted as theGraphQLInterfacetype.The
@composedecorator designates whichInterfaces should the current model be composed of. The@composedecorator can only refer to other models that are marked with the@Interfacedecorator and not vanilla model types.Mapping
@InterfaceinterfaceModelinterface (Output Type)@composeextends Iface1, Iface2…Decorators
@Interface@composecomposemust be present in the target modelExamples
Fields within the composed model can be defined using either
...operator or manually, both are validGraphQL requires both Person and Node to be explicitly implemented by Actor.
Design Alternatives
composeautomatically – this wouldn’t be great because thencomposewould change the shape of the model just for GraphQLInterfaceand assume interfaces from models used incompose. Since GraphQL has an explicit concept ofInterfacewe’re representing that using this decorator. If validation rules specific toInterfaces need to be applied in the future, it will be possible to do soEnums
Warning
This section is under review and possible reconsideration.
Context and design challenges
TSP enum member types have no meaning in GraphQL and the enum member values should follow the naming convention shown below (similar to all other literal names). From the GraphQL spec: “EnumValue
Name but not true false null”
where Name should start with [A-Za-z] or <underscore> and can be followed by letter, digit, or <underscore>
GraphQL Recommendation: “It is recommended that Enum values be “all caps”. Enum values are only used in contexts where the precise enumeration type is known. Therefore it’s not necessary to supply an enumeration type name in the literal.”
Design Proposal
Use TypeSpec enums in the value context as GraphQL doesn’t need the type information.
TypeSpec enums with no types that can only be identifiers or string literals will be translated to all caps GraphQL enums as long as the identifiers are valid GraphQL names. If they are invalid, the emitter will throw a validation error.
🔴 Design decision: TypeSpec enums with integer or floating point values will be converted to a string value using the following rules to create
result:resultto_NEGATIVE_to the result string.is converted to an_resultPros: The GraphQL enum is a string representation of the
valueand reflects the true intention of the developerCons: The server side implementation will have to figure out the translation between the GraphQL enum and the internal representation of the enum where the algorithm isn’t obvious (i.e. they will basically have to implement the steps above).
Inline enums that don’t have an enum name will be assigned a distinct name based on where the field appears in the TSP schema. The name derived from the field will be followed by an
Enumsuffix. To provide disambiguation, the full name should benamespace+modelName+fieldName. See the examples table for an example.Mapping
Enum.nameEnum.nameEnum.membersEnum.membersExamples
Convert the hour values into GraphQL enum values
Note that we don’t use the type as TSP types might only have meaning within the TSP code and not the emitted protocol
Convert Boundary values into GraphQL enum values
Derive a unique name based on the namespace, model, field name \+ “Enum”
Design Alternatives
Anyfor enums with values as integers or floating points and let the developer define an alternate type using visibility.@invisibledecorator can be applied toEnumMembers, we can provide alternate enum members for GraphQL in the same enum definition which change the emitter to emit the GraphQL enum values as shown below:==================================== GRAPHQL ====================================
Operations
Context and design challenges
There are three kinds of GraphQL Operations: Query, Mutation and Subscription. While in TypeSpec there is no difference between them.
Design Proposal
Warning
This section is under review. The means of associating operations with a given operation type may change.
To distinguish between Queries, Mutations and Subscription, we are proposing to include a set of three decorators in TypeSpec:
@query,@mutationand@subscription. These will decorate the TSP Operations to indicate the GraphQL kind.The decorators would also be added to an interface, understanding that all operations within the interface would be of the provided kind.
The GraphQL emitter will generate the proper GraphQL kind for each Operation, according to these rules:
@query,@mutation,@subscriptionThe Operation parameters will be converted to GraphQL arguments following the rules for the GraphQL Input types.
The Operation return type should be a valid GraphQL Output Type.
In line with the Field Arguments design, the operations decorated directly or indirectly with the @operationFields decorator, would not be added as query, mutations or subscriptions.
When no operation is emitted, an empty schema will be generated.
When mutations are provided, but there are no query operations, a dummy Query will be added to the schema to make it valid.
Mapping
@GraphQL.query@GraphQL.mutation@GraphQL.subscriptionTypeOperation.namenameOperation.returnTypetypeOperation.parametersargsDecorators
@queryOperation,Interface@mutationOperation,Interface@subscriptionOperation,InterfaceExamples
Decorator Validation Errors
Lists
Context and design challenges
TSP defines a
listandArraybuiltin types and both of those need to be converted to GraphQL lists. GraphQL lists are wrappers over output and input types.Design Proposal
For TSP lists (
[]) and arrays (Array) used as types of properties, parameters and operations, we will emit the corresponding list of types in GraphQL.Mapping
List.typeList.typeArray.typeList.typeExamples
Note the difference in the requiredness of the values vs the list itself for the various options
Nullable vs Optional
Warning
This section is under review. The approach described here will be overhauled if our Contextual Requiredness proposal is accepted.
Context and design challenges
In GraphQL, all properties and parameters are nullable by default, and the
!operator is applied to indicate non-nullability.And although all fields are optional; for parameters, Input fields are required if they are marked as non-nullable.
In TypeSpec non-nullable is the default, while nullability is expressed by an Union that includes the
nulltype. Also in TypeSpec: all the fields are required, unless are marked optional with the?operator.Design Proposal
All output types and return types will be emitted in GraphQL as non-nullable (
!operator), except when the field is marked as optional, or when the type of the field is an Union containing the TypeSpecnulltype.We can also use the same rules for Input fields, but we will force the field as required if the property or the argument is not nullable. Alternatively, we can throw an error.
name: string;name: String!name: String!name?: string;name: Stringname: String!name: string | null;name: Stringname: Stringname?: string | null;name: Stringname: StringExamples
Design Alternatives
Visibility & Never
Context and design challenges
@visibilty,@invisible,@withVisibility, et al decorators.nevertype@withVisibilityis already considered in the compiler, so it will be also included in the emitter.never.Design Proposal
Add to the emitter the handling of the
nevertype, and exclude any field from the Model before emitting the Model.Note: This may result in empty models. We need to define what to do with fields pointing to empty Models.
Create a new visibility class named
OperationType:For implicit filtered models (automatic visibility):
GraphQL does not have an equivalent concept like HTTP verbs that map to the
Lifecyclevisibility modifiers. However, GraphQL mutations will commonly adhere to these type of "CRUD" operations.TSP developers will need to take advantage of the
@parameterVisibilityand@returnTypeVisibilitydecorators to filter the models based on the semantic operation type.In the case where the operation does not have explicit visibility specified and is already decorated with an HTTP verb, the emitter will use the HTTP library specification to apply the related visibility to the input types.
If none of the standard "CRUD" operations apply, whether the operation is a query, mutation, or subscription will apply the
OperationType.Query,OperationType.Mutation, orOperationType.Subscriptionvisibility to input types, respectively.For practical reasons, we will follow lead of the HTTP library on response types and filter them to
Lifecycle.Readby default.Generated model names will be suffixed with the appropriate operation type, e.g.
UserQueryInput,UserRead,UserCreateInput,UserMutationInput, etc.The new models would be generated only if they are distinct from the original Model.
Examples
Open Questions
<Model>Readtypes will be the most common; should we have theLifecycle.Read-filtered model instead be called<Model>, and the unfiltered model be something like<Model>Full?User feedback:
The emitter will generate feedback for the developers through errors and warnings. But the warning list could be enormous and not easy to read, especially when trying to emit a GraphQL from a large TSP specification not specifically designed for GraphQL.
With this in mind we are proposing to emit a "How to improve your TypeSpec scheme for GraphQL" report based on the warnings and other signals. The purpose is to help developers to generate a better GraphQL schema, introducing the GraphQL decorators and other tricks to their TypeSpec code. The report should be more readable than the warnings.
Typespec extension suggestions