Generated Code Reference (protobuf-es)
Detailed reference for the TypeScript code generated by protoc-gen-pothos with protobuf-es (v2) runtime.
Overview
protobuf-es v2 introduces a Schema-based API where messages are plain objects with associated Schema descriptors. The key differences from v1:
| Feature | protobuf-es-v1 | protobuf-es (v2) |
|---|---|---|
| Message type | Class | Plain object + Schema |
| Type check | instanceof | isMessage(source, Schema) |
| Message creation | new Message({...}) | create(Schema, {...}) |
| Import pattern | import { User } from "...user_pb" | import { User, UserSchema } from "...user_pb" |
File Structure
For each .proto file, the plugin generates a .pb.pothos.ts file containing:
proto/example/user.proto
|
src/__generated__/example/user.pb.pothos.tsEach generated file includes:
- Import statements (with
_pbsuffix, including Schema symbols) - Object type definitions (
$Ref) - Input type definitions (
$Shape,$Ref) - Enum type definitions
- Union type definitions (for oneofs)
- Helper functions (
$toProto)
Naming Conventions
Export Identifiers
| Proto Definition | Generated Export | Type |
|---|---|---|
message User | User$Ref | ObjectRef<User> |
message User (input) | UserInput$Shape | Type alias |
message User (input) | UserInput$Ref | InputObjectRef<UserInput$Shape> |
message User (input) | UserInput$toProto | Function |
message User (partial) | UserPartialInput$Shape | Type alias |
message User (partial) | UserPartialInput$Ref | InputObjectRef<...> |
enum Role | Role$Ref | EnumRef<Role, Role> |
oneof content in Media | MediaContent$Ref | Union type |
Nested Messages
Nested messages use underscore-separated naming:
message User {
message Address {
string city = 1;
}
}Generated exports:
UserAddress$RefUserAddressInput$ShapeUserAddressInput$RefUserAddressInput$toProto
Object Type Generation
Basic Object Type
message Primitives {
// Required.
int32 required_int32_value = 1;
// Required.
string required_string_value = 2;
// Required.
bool required_bool_value = 3;
}import { Primitives, PrimitivesSchema } from "@example/proto/primitives_pb";
import { isMessage } from "@bufbuild/protobuf";
export const Primitives$Ref = builder.objectRef<Primitives>("Primitives");
builder.objectType(Primitives$Ref, {
name: "Primitives",
fields: (t) => ({
requiredInt32Value: t.expose("requiredInt32Value", {
type: "Int",
nullable: false,
extensions: {
protobufField: { name: "required_int32_value", typeFullName: "int32" },
},
}),
requiredStringValue: t.expose("requiredStringValue", {
type: "String",
nullable: false,
extensions: {
protobufField: { name: "required_string_value", typeFullName: "string" },
},
}),
requiredBoolValue: t.expose("requiredBoolValue", {
type: "Boolean",
nullable: false,
extensions: {
protobufField: { name: "required_bool_value", typeFullName: "bool" },
},
}),
}),
isTypeOf: (source) => {
return isMessage(source, PrimitivesSchema);
},
extensions: {
protobufMessage: {
fullName: "example.Primitives",
name: "Primitives",
package: "example",
},
},
});isTypeOf Implementation
protobuf-es v2 uses isMessage() function with Schema for type discrimination:
import { isMessage } from "@bufbuild/protobuf";
import { UserSchema } from "./user_pb";
isTypeOf: (source) => {
return isMessage(source, UserSchema);
}This is different from v1 which uses instanceof:
// v1 (for comparison)
isTypeOf: (source) => {
return source instanceof User;
}Field Methods
| Method | Usage |
|---|---|
t.expose() | Direct field exposure without resolver |
t.field() | Field with custom resolver |
t.expose() is used when the field value can be accessed directly from the source object.
t.field() is used when any of the following conditions apply:
- Bytes fields: Require
Bufferconversion - Enum fields with unspecified/ignored values: Need resolver for value handling
- Oneof fields: Access oneof member values
- Squashed oneof union fields: Flatten nested oneof to parent type
- Required nested message fields: Need non-null assertion (
!) - Required repeated fields: Need non-null assertion for list
Empty Message Type
Messages with no fields generate a noop field to satisfy GraphQL schema requirements:
export const EmptyMessage$Ref = builder.objectRef<EmptyMessage>("EmptyMessage");
builder.objectType(EmptyMessage$Ref, {
name: "EmptyMessage",
fields: (t) => ({
_: t.field({
type: "Boolean",
nullable: true,
description: "noop field",
resolve: () => true,
}),
}),
isTypeOf: (source) => {
return isMessage(source, EmptyMessageSchema);
},
extensions: {
protobufMessage: {
fullName: "example.EmptyMessage",
name: "EmptyMessage",
package: "example",
},
},
});Input Type Generation
Basic Input Type
export type PrimitivesInput$Shape = {
requiredInt32Value: Primitives["requiredInt32Value"];
requiredStringValue: Primitives["requiredStringValue"];
requiredBoolValue: Primitives["requiredBoolValue"];
};
export const PrimitivesInput$Ref: InputObjectRef<PrimitivesInput$Shape> =
builder.inputRef<PrimitivesInput$Shape>("PrimitivesInput").implement({
fields: (t) => ({
requiredInt32Value: t.field({
type: "Int",
required: true,
extensions: {
protobufField: { name: "required_int32_value", typeFullName: "int32" },
},
}),
requiredStringValue: t.field({
type: "String",
required: true,
extensions: {
protobufField: { name: "required_string_value", typeFullName: "string" },
},
}),
requiredBoolValue: t.field({
type: "Boolean",
required: true,
extensions: {
protobufField: { name: "required_bool_value", typeFullName: "bool" },
},
}),
}),
extensions: {
protobufMessage: {
fullName: "example.Primitives",
name: "Primitives",
package: "example",
},
},
});Shape Type
The $Shape type alias derives field types from the protobuf message type:
export type MessageInput$Shape = {
requiredPrimitives: PrimitivesInput$Shape; // Required nested message
optionalPrimitives?: PrimitivesInput$Shape | null; // Optional nested message
};Partial Input Type
When partial_inputs=true is configured:
export type PrimitivesPartialInput$Shape = {
requiredInt32Value?: Primitives["requiredInt32Value"] | null;
requiredStringValue?: Primitives["requiredStringValue"] | null;
requiredBoolValue?: Primitives["requiredBoolValue"] | null;
};
export const PrimitivesPartialInput$Ref: InputObjectRef<PrimitivesPartialInput$Shape> =
builder.inputRef<PrimitivesPartialInput$Shape>("PrimitivesPartialInput").implement({
fields: (t) => ({
requiredInt32Value: t.field({
type: "Int",
required: false, // All fields optional
extensions: {
protobufField: { name: "required_int32_value", typeFullName: "int32" },
},
}),
// ...
}),
// ...
});toProto Functions
protobuf-es v2 uses create() function with Schema for message creation. The generated $toProto helper functions convert GraphQL input to protobuf messages.
Simple Message
import { create } from "@bufbuild/protobuf";
import { PrimitivesSchema } from "./primitives_pb";
export function PrimitivesInput$toProto(
input: PrimitivesInput$Shape | null | undefined,
): Primitives {
return create(PrimitivesSchema, {
requiredInt32Value: input?.requiredInt32Value ?? undefined,
requiredStringValue: input?.requiredStringValue ?? undefined,
requiredBoolValue: input?.requiredBoolValue ?? undefined,
});
}This is different from v1 which uses class constructor:
// v1 (for comparison)
export function PrimitivesInput$toProto(
input: PrimitivesInput$Shape | null | undefined,
): Primitives {
return new Primitives({
requiredInt32Value: input?.requiredInt32Value ?? undefined,
// ...
});
}Nested Message
export function MessageInput$toProto(
input: MessageInput$Shape | null | undefined,
): Message {
return create(MessageSchema, {
requiredPrimitives: input?.requiredPrimitives
? PrimitivesInput$toProto(input.requiredPrimitives)
: undefined,
optionalPrimitives: input?.optionalPrimitives
? PrimitivesInput$toProto(input.optionalPrimitives)
: undefined,
requiredPrimitivesList: input?.requiredPrimitivesList?.map((v) =>
PrimitivesInput$toProto(v)
),
optionalPrimitivesList: input?.optionalPrimitivesList?.map((v) =>
PrimitivesInput$toProto(v)
),
});
}Oneof Fields
Oneof fields use the { case, value } format:
export function OneofParentInput$toProto(
input: OneofParentInput$Shape | null | undefined,
): OneofParent {
return create(OneofParentSchema, {
normalField: input?.normalField ?? undefined,
requiredOneofMembers: input?.requiredMessage1
? {
case: "requiredMessage1",
value: OneofMemberMessage1Input$toProto(input.requiredMessage1),
}
: input?.requiredMessage2
? {
case: "requiredMessage2",
value: OneofMemberMessage2Input$toProto(input.requiredMessage2),
}
: undefined,
});
}Enum Type Generation
Basic Enum
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
MY_ENUM_FOO = 1;
MY_ENUM_BAR = 2;
}import { MyEnum } from "./enums_pb";
import { EnumRef } from "@pothos/core";
export const MyEnum$Ref: EnumRef<MyEnum, MyEnum> = builder.enumType("MyEnum", {
values: {
FOO: {
value: 1,
extensions: { protobufEnumValue: { name: "MY_ENUM_FOO" } },
},
BAR: {
description: "This is Bar.",
value: 2,
extensions: { protobufEnumValue: { name: "MY_ENUM_BAR" } },
},
} as const,
extensions: {
protobufEnum: {
name: "MyEnum",
fullName: "testapi.enums.MyEnum",
package: "testapi.enums",
},
},
});UNSPECIFIED Value Handling
Values matching <ENUM_NAME>_UNSPECIFIED at position 0 are excluded from GraphQL enum values.
When a field references an enum with an unspecified value, a resolver is generated:
requiredMyEnum: t.field({
type: MyEnum$Ref,
nullable: false,
description: "Required.",
resolve: (source) => {
if (source.requiredMyEnum === MyEnum.UNSPECIFIED) {
throw new Error(
"requiredMyEnum is required field. But got unspecified.",
);
}
return source.requiredMyEnum;
},
extensions: {
protobufField: {
name: "required_my_enum",
typeFullName: "testapi.enums.MyEnum",
},
},
}),
optionalMyEnum: t.field({
type: MyEnum$Ref,
nullable: true,
description: "Optional.",
resolve: (source) => {
if (source.optionalMyEnum === MyEnum.UNSPECIFIED) {
return null; // nullable field returns null for unspecified
}
return source.optionalMyEnum;
},
// ...
}),IGNORED Value Handling
When an enum value is marked with the ignore option, the resolver throws an error:
prefixedEnum: t.field({
type: TestPrefixPrefixedEnum$Ref,
nullable: true,
resolve: (source) => {
if (source.prefixedEnum === PrefixedEnum.PREFIXED_ENUM_UNSPECIFIED) {
return null;
}
if (source.prefixedEnum === PrefixedEnum.PREFIXED_IGNORED) {
throw new Error("PREFIXED_IGNORED is ignored in GraphQL schema");
}
return source.prefixedEnum;
},
// ...
}),Union Type Generation (Oneofs)
Basic Union
message OneofParent {
oneof required_oneof_members {
OneofMemberMessage1 required_message1 = 1;
OneofMemberMessage2 required_message2 = 2;
}
}export const OneofParentRequiredOneofMembers$Ref = builder.unionType(
"OneofParentRequiredOneofMembers",
{
types: [OneofMemberMessage1$Ref, OneofMemberMessage2$Ref],
description: "Required. disallow not_set.",
extensions: {
protobufOneof: {
fullName: "testapis.oneof.OneofParent.required_oneof_members",
name: "required_oneof_members",
messageName: "OneofParent",
package: "testapis.oneof",
fields: [{
name: "required_message1",
type: "testapis.oneof.OneofMemberMessage1",
}, {
name: "required_message2",
type: "testapis.oneof.OneofMemberMessage2",
}],
},
},
},
);Oneof Field Resolver
protobuf-es accesses oneof values through the oneof name property:
requiredOneofMembers: t.field({
type: OneofParentRequiredOneofMembers$Ref,
nullable: false,
description: "Required. disallow not_set.",
resolve: (source) => {
const value = source.requiredOneofMembers.value;
if (value == null) {
throw new Error("requiredOneofMembers should not be null");
}
return value;
},
extensions: { protobufField: { name: "required_oneof_members" } },
}),
optionalOneofMembers: t.field({
type: OneofParentOptionalOneofMembers$Ref,
nullable: true,
resolve: (source) => {
return source.optionalOneofMembers.value; // returns undefined if not set
},
extensions: { protobufField: { name: "optional_oneof_members" } },
}),Squashed Oneof Union
When a message contains only a oneof field and is marked with squashUnion: true option:
message PrefixedMessage {
message SquashedMessage {
oneof content {
option (graphql.oneof).squash_union = true;
InnerMessage oneof_field = 1 [(graphql.object_type).squash_union = true];
InnerMessage2 oneof_field_2 = 2 [(graphql.object_type).squash_union = true];
}
}
SquashedMessage squashed_message = 5;
}Generated field resolver:
squashedMessage: t.field({
type: TestPrefixPrefixedMessageSquashedMessage$Ref,
nullable: true,
resolve: (source) => {
const value = source.squashedMessage?.content.value;
if (value == null) {
return null;
}
return value;
},
extensions: {
protobufField: {
name: "squashed_message",
typeFullName: "testapis.extensions.PrefixedMessage.SquashedMessage",
},
},
}),Field Resolver Patterns
Bytes Field
Bytes fields require Buffer conversion:
requiredBytesValue: t.field({
type: "Byte",
nullable: false,
resolve: (source) => {
return Buffer.from(source.requiredBytesValue);
},
extensions: {
protobufField: { name: "required_bytes_value", typeFullName: "bytes" },
},
}),Repeated Bytes Field
requiredBytesValues: t.field({
type: ["Byte"],
nullable: { list: false, items: false },
resolve: (source) => {
return source.requiredBytesValues.map((v) => Buffer.from(v));
},
extensions: {
protobufField: { name: "required_bytes_values", typeFullName: "bytes" },
},
}),Nested Message Field
Optional nested message fields use t.expose():
optionalPrimitives: t.expose("optionalPrimitives", {
type: Primitives$Ref,
nullable: true,
description: "Optional.",
extensions: {
protobufField: {
name: "optional_primitives",
typeFullName: "testapis.primitives.Primitives",
},
},
}),Required Nested Message Field
Required nested message fields use t.field() with non-null assertion:
requiredPrimitives: t.field({
type: Primitives$Ref,
nullable: false,
description: "Required.",
resolve: (source) => {
return source.requiredPrimitives!;
},
extensions: {
protobufField: {
name: "required_primitives",
typeFullName: "testapis.primitives.Primitives",
},
},
}),Repeated Field Nullability
For list (repeated) fields, the nullable property uses an object structure:
// Required list - list and items are never null
requiredPrimitivesList: t.field({
type: [Primitives$Ref],
nullable: { list: false, items: false },
description: "Required.",
resolve: (source) => {
return source.requiredPrimitivesList!;
},
extensions: {
protobufField: {
name: "required_primitives_list",
typeFullName: "testapis.primitives.Primitives",
},
},
}),
// Optional list - list can be null, but items are never null
optionalPrimitivesList: t.expose("optionalPrimitivesList", {
type: [Primitives$Ref],
nullable: { list: true, items: false },
description: "Optional.",
extensions: {
protobufField: {
name: "optional_primitives_list",
typeFullName: "testapis.primitives.Primitives",
},
},
}),| Nullable Value | GraphQL Type |
|---|---|
{ list: false, items: false } | [Type!]! |
{ list: true, items: false } | [Type!] |
64-bit Integer Fields
protobuf-es uses the Int64 scalar for 64-bit integers:
requiredInt64Value: t.expose("requiredInt64Value", {
type: "Int64",
nullable: false,
extensions: {
protobufField: { name: "required_int64_value", typeFullName: "int64" },
},
}),Supported 64-bit types: int64, uint64, sint64, fixed64, sfixed64
Import Patterns
Generated File Imports
// @generated by protoc-gen-pothos v0.6.2 with parameter "protobuf_lib=protobuf-es,..."
import { builder } from "../../builder";
// Import message types AND Schema symbols
import {
User,
UserSchema,
Address,
AddressSchema,
} from "@example/proto/user_pb";
// Import create and isMessage from @bufbuild/protobuf
import { create, isMessage } from "@bufbuild/protobuf";
// Import Pothos types
import { InputObjectRef, EnumRef } from "@pothos/core";Key Import Differences from v1
| Import | v1 | v2 |
|---|---|---|
| Message type | import { User } from "...user_pb" | import { User, UserSchema } from "...user_pb" |
| Type utilities | N/A | import { create, isMessage } from "@bufbuild/protobuf" |
Extensions Object
Generated code includes extensions objects for introspection:
protobufMessage
extensions: {
protobufMessage: {
fullName: "testapis.primitives.Primitives",
name: "Primitives",
package: "testapis.primitives",
},
}protobufField
extensions: {
protobufField: {
name: "required_primitives",
typeFullName: "testapis.primitives.Primitives",
},
}protobufEnum
extensions: {
protobufEnum: {
name: "MyEnum",
fullName: "testapi.enums.MyEnum",
package: "testapi.enums",
},
}protobufEnumValue
extensions: {
protobufEnumValue: {
name: "MY_ENUM_FOO",
options: { deprecated: true }, // if applicable
},
}protobufOneof
extensions: {
protobufOneof: {
fullName: "testapis.oneof.OneofParent.required_oneof_members",
name: "required_oneof_members",
messageName: "OneofParent",
package: "testapis.oneof",
fields: [
{ name: "required_message1", type: "testapis.oneof.OneofMemberMessage1" },
{ name: "required_message2", type: "testapis.oneof.OneofMemberMessage2" },
],
},
}