Protobuf to GraphQL Converter
Paste a .proto schema. Get GraphQL SDL with types, enums, non-null lists, and synthetic entry types for map fields — ready to seed a gateway schema.
Input (.proto schema)
Output (GraphQL SDL)
What this tool does
You have a Protocol Buffers schema and a GraphQL gateway in front of it (or you are about to build one). Tools like Google's rejoiner wire proto messages into a live GraphQL endpoint at runtime, but for sketching the schema, code review, or seeding a hand-written resolver layer, you usually just want to see what the SDL looks like. This converter does that in your browser — paste, copy the output, drop it into schema.graphql.
Each message becomes a GraphQL type; each enum becomes a GraphQL enum. Field names are converted from snake_case (proto convention) to camelCase (the convention encoded in the GraphQL spec and enforced by every linter). Scalars and enums are emitted as non-null (String!, OrderStatus!) because proto3 fields always have a value on the wire — even unset ones default to the zero value. Singular nested messages are emitted as nullable, which matches proto3 has-value semantics.
Repeated fields render as [T!]! — non-null list of non-null elements — because proto3 repeated fields are never null and never contain null entries. Maps need a workaround: GraphQL has no native map type, so map<string, string> on a metadata field becomes [OrderMetadataEntry!]! with a synthetic type OrderMetadataEntry { key: String! value: String! }, the same pattern used by Apollo and most production proto-to-GraphQL gateways. The whole conversion runs client-side; nothing about your schema leaves the page.
How to use it
Three steps. Output is paste-ready for any GraphQL server that accepts SDL.
Paste your .proto schema
Drop the schema into the left editor. syntax = "proto3"; at the top is optional — the parser handles nested message blocks, enum declarations, oneof, map<K, V>, and field options. Imports are recognised but skipped, so paste imported types inline if you depend on them.
Field name conversion is automatic: order_id in .proto becomes orderId in GraphQL. Type and enum names stay PascalCase. Map fields get a synthetic entry type named <Parent><Field>Entry.
Read the output
On the right: GraphQL SDL with enums first, then map-entry types, then messages in declaration order. Nested types come before their parents so the file reads top-down. Drop it into a .graphql file your server loads via graphql-tools or buildSchema.
Wire up resolvers
The schema gives you the shape; you still need to write resolvers (or generate them from your gRPC service). For a quick path, point an Apollo Server at this SDL with stub resolvers, then replace each stub with a call to your gRPC backend. Adjust nullability if your runtime contract differs from proto3 defaults.
When this actually saves time
Bootstrapping a GraphQL gateway over a gRPC backend
Your team has a gRPC service and product wants a GraphQL endpoint for the web client. You need a starting-point schema to discuss with frontend engineers before writing any resolvers. Paste the proto, copy the SDL, drop it in a doc — done.
Reviewing a Protobuf API change
A backend teammate added fields to a message. You want to see how that affects the public GraphQL surface without rerunning a full codegen pipeline. Paste the new .proto, diff the SDL output against your current schema, leave a focused review comment.
Documentation and design discussions
You are writing an RFC about a new gRPC service that will eventually need a GraphQL fronting. Embedding both shapes in the doc — proto on one side, SDL on the other — makes the conversation concrete. This converter gets you the SDL side without standing up a build.
Migrating from REST or gRPC to GraphQL
You inherited a Protobuf-defined service and the new product brief calls for a GraphQL API. Generate a draft schema here as the starting point for the migration, then iterate on field names, nullability, and pagination by hand.
Common questions
Is my schema sent anywhere?
No. The .proto parser and the SDL emitter both run as JavaScript in your browser. Open DevTools and watch the Network tab while you paste — zero requests. Useful when your schema includes internal type names, package paths, or anything you would not want to ship to a third-party service.
Why are 64-bit ints typed as String?
GraphQL's built-in Int scalar is signed 32-bit per the GraphQL spec, which cannot hold a proto int64 or uint64. The conventional workaround is to define a custom scalar (often called BigInt, Long, or Int64) and serialise the value as a string. This converter emits String for all 64-bit integer types so the schema is valid out of the box; replace those occurrences with your custom scalar name once you have one defined on the server.
How are map fields handled?
GraphQL has no native map type — there is no { String: String } shape. The standard workaround is to unroll the map into a list of { key, value } pairs. So map<string, string> metadata = 8; on a message Order becomes metadata: [OrderMetadataEntry!]! with a synthetic type OrderMetadataEntry { key: String! value: String! }. The entry type name is derived from the parent message + field name in PascalCase + Entry, which is the convention used by most proto-to-GraphQL gateways including rejoiner.
How is oneof handled?
Each oneof field is emitted as a regular nullable field with a comment marking the group. GraphQL does not have a native discriminated-union concept that maps cleanly to proto oneof — the closest analogue is a custom union type or an @oneOf input directive (recently added to the spec for inputs). For output types most schemas just emit each oneof case as nullable and document the constraint, which is what we do here. Edit the output by hand if you want strict union types.
Why are scalars non-null but messages nullable?
This matches proto3 semantics. Scalars in proto3 always have a value on the wire — an unset string field defaults to "", an unset int32 defaults to 0. There is no way to distinguish "set to default" from "unset" for scalars without using optional or wrapper types. Singular nested messages, however, do have has-value semantics — the field tag may be entirely missing — so they map naturally to nullable GraphQL fields. Repeated fields are always non-null lists with non-null elements, again matching proto3.
What about google.protobuf.Timestamp and other well-known types?
Well-known types are emitted as String by default, since GraphQL has no built-in DateTime scalar. Most production schemas define a custom DateTime (or ISO8601String) scalar and replace the String occurrences after generation. The same applies to google.protobuf.Duration, Any, and Value — emit as String first, swap in custom scalars once defined.
Bytes fields — why String?
GraphQL has no native binary scalar. The conventional encoding is base64 in a String field, which is also what the proto3 JSON mapping uses for bytes. If you want stricter typing, define a Base64String custom scalar on the server and replace String on those fields.
Related tools
If you are working with Protobuf, GraphQL, and JSON, these pair well: