Generated Code Reference (protobuf-es-v1)
Detailed reference for the TypeScript code generated by protoc-gen-pothos with protobuf-es-v1 runtime.
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) - 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 Date {
uint32 year = 1;
uint32 month = 2;
uint32 day = 3;
}import { Date } from "@testapis/protobuf-es/testapis/custom_types/date_pb";
export const Date$Ref = builder.objectRef<Date>("Date");
builder.objectType(Date$Ref, {
name: "Date",
fields: (t) => ({
year: t.expose("year", {
type: "Int",
nullable: false,
extensions: { protobufField: { name: "year", typeFullName: "uint32" } },
}),
month: t.expose("month", {
type: "Int",
nullable: false,
extensions: { protobufField: { name: "month", typeFullName: "uint32" } },
}),
day: t.expose("day", {
type: "Int",
nullable: false,
extensions: { protobufField: { name: "day", typeFullName: "uint32" } },
}),
}),
isTypeOf: (source) => {
return source instanceof Date;
},
extensions: {
protobufMessage: {
fullName: "testapis.custom_types.Date",
name: "Date",
package: "testapis.custom_types",
},
},
});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
isTypeOf Implementation
protobuf-es uses instanceof for type discrimination:
isTypeOf: (source) => {
return source instanceof MessageClass;
}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 source instanceof EmptyMessage;
},
extensions: {
protobufMessage: {
fullName: "testapis.empty_types.EmptyMessage",
name: "EmptyMessage",
package: "testapis.empty_types",
},
},
});Input Type Generation
Basic Input Type
export type DateInput$Shape = {
year: Date["year"];
month: Date["month"];
day: Date["day"];
};
export const DateInput$Ref: InputObjectRef<DateInput$Shape> = builder.inputRef<
DateInput$Shape
>("DateInput").implement({
fields: (t) => ({
year: t.field({
type: "Int",
required: true,
extensions: { protobufField: { name: "year", typeFullName: "uint32" } },
}),
month: t.field({
type: "Int",
required: true,
extensions: { protobufField: { name: "month", typeFullName: "uint32" } },
}),
day: t.field({
type: "Int",
required: true,
extensions: { protobufField: { name: "day", typeFullName: "uint32" } },
}),
}),
extensions: {
protobufMessage: {
fullName: "testapis.custom_types.Date",
name: "Date",
package: "testapis.custom_types",
},
},
});Shape Type
The $Shape type alias derives field types from the protobuf message type:
export type PostInput$Shape = {
title: Post["title"]; // Required field
publishedDate?: DateInput$Shape | null; // Optional nested message
};Partial Input Type
When partial_inputs=true is configured:
export type DatePartialInput$Shape = {
year?: Date["year"] | null;
month?: Date["month"] | null;
day?: Date["day"] | null;
};
export const DatePartialInput$Ref: InputObjectRef<DatePartialInput$Shape> =
builder.inputRef<DatePartialInput$Shape>("DatePartialInput").implement({
fields: (t) => ({
year: t.field({
type: "Int",
required: false, // All fields optional
extensions: { protobufField: { name: "year", typeFullName: "uint32" } },
}),
// ...
}),
// ...
});toProto Functions
protobuf-es generates $toProto helper functions for converting GraphQL input to protobuf messages.
Simple Message
export function DateInput$toProto(
input: DateInput$Shape | null | undefined,
): Date {
return new Date({
year: input?.year ?? undefined,
month: input?.month ?? undefined,
day: input?.day ?? undefined,
});
}Nested Message
export function PostInput$toProto(
input: PostInput$Shape | null | undefined,
): Post {
return new Post({
title: input?.title ?? undefined,
publishedDate: input?.publishedDate
? DateInput$toProto(input.publishedDate)
: undefined,
});
}Oneof Fields
export function OneofParentInput$toProto(
input: OneofParentInput$Shape | null | undefined,
): OneofParent {
return new OneofParent({
normalField: input?.normalField ?? undefined,
requiredOneofMembers: input?.requiredMessage1
? { case: "requiredMessage1", value: Message1Input$toProto(input.requiredMessage1) }
: input?.requiredMessage2
? { case: "requiredMessage2", value: Message2Input$toProto(input.requiredMessage2) }
: undefined,
});
}Enum Type Generation
Basic Enum
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
MY_ENUM_FOO = 1;
MY_ENUM_BAR = 2;
}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:
nestedEnum: t.field({
type: ParentMessageNestedEnum$Ref,
nullable: true,
resolve: (source) => {
if (source.nestedEnum === ParentMessage_NestedEnum.NESTED_ENUM_UNSPECIFIED) {
return null;
}
return source.nestedEnum;
},
// ...
}),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 content {
InnerMessage1 msg1 = 1;
InnerMessage2 msg2 = 2;
}
}export const OneofParentContent$Ref = builder.unionType(
"OneofParentContent",
{
types: [InnerMessage1$Ref, InnerMessage2$Ref],
extensions: {
protobufOneof: {
fullName: "testapis.oneof.OneofParent.content",
name: "content",
messageName: "OneofParent",
package: "testapis.oneof",
fields: [{
name: "msg1",
type: "testapis.oneof.InnerMessage1",
}, {
name: "msg2",
type: "testapis.oneof.InnerMessage2",
}],
},
},
},
);Oneof Field Resolver
protobuf-es accesses oneof values through the oneof name property:
content: t.field({
type: OneofParentContent$Ref,
nullable: true,
resolve: (source) => {
return source.content.value ?? null;
},
extensions: { protobufField: { name: "content" } },
}),Required Oneof
When marked with // Required.:
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" } },
}),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 union type:
export const TestPrefixPrefixedMessageSquashedMessage$Ref = builder.unionType(
"TestPrefixPrefixedMessageSquashedMessage",
{
types: [
TestPrefixPrefixedMessageInnerMessage$Ref,
TestPrefixPrefixedMessageInnerMessage2$Ref,
],
extensions: {
protobufOneof: {
fullName: "testapis.extensions.PrefixedMessage.SquashedMessage",
name: "SquashedMessage",
package: "testapis.extensions",
fields: [{
name: "oneof_field",
type: "testapis.extensions.PrefixedMessage.InnerMessage",
options: { "[graphql.object_type]": { squashUnion: true } },
}, {
name: "oneof_field_2",
type: "testapis.extensions.PrefixedMessage.InnerMessage2",
options: { "[graphql.object_type]": { squashUnion: true } },
}],
},
},
},
);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:
data: t.field({
type: "Byte",
nullable: true,
resolve: (source) => {
return source.data == null ? null : Buffer.from(source.data);
},
}),Repeated Bytes Field
dataList: t.field({
type: ["Byte"],
nullable: { list: false, items: false },
resolve: (source) => {
return source.dataList.map((v) => Buffer.from(v));
},
}),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" },
},
}),Extensions Object
Generated code includes extensions objects for introspection:
protobufMessage
extensions: {
protobufMessage: {
fullName: "testapis.custom_types.Date",
name: "Date",
package: "testapis.custom_types",
},
}protobufField
extensions: {
protobufField: {
name: "published_date",
typeFullName: "testapis.custom_types.Date",
},
}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.Media.content",
name: "content",
messageName: "Media",
package: "testapis.oneof",
fields: [
{ name: "image", type: "testapis.oneof.Image" },
{ name: "video", type: "testapis.oneof.Video" },
],
},
}