Skip to content

Latest commit

 

History

History
230 lines (157 loc) · 5.02 KB

schema-conversion.md

File metadata and controls

230 lines (157 loc) · 5.02 KB

Protobuf -> GraphQL Schema Conversion

Objects that can be converted almost as-is

These Protobuf objects can be converted to GraphQL objects almost as-is.

Protobuf message -> GraphQL type

Protobuf:

message Message {
  string value = 1;
}

GraphQL:

type Message {
  value: String!
}

Protobuf enum -> GraphQL enum

Protobuf:

enum Enum {
  A = 1;
  B = 2;
}

GraphQL:

enum Enum {
  A
  B
}

Objects that cannot be converted as-is

Protobuf oneof -> GraphQL union

Problem

GraphQL union is similar to Protobuf oneof, but the type of a field in a GraphQL union must be different from other fields.

For example, the following Protobuf oneof cannot be converted directly to GraphQL union:

oneof OneOf {
  string A = 1;
  string B = 2;
}
# ERROR!
union OneOf = String | String

Solution in this implementation

In this implementation, we avoided this problem by defining a new type for each field.

union OneOf = OneOfA | OneOfB
type OneOfA {
  a: String!
}
type OneOfB {
  b: String!
}

The original field name is still there as the suffix of the generated type and its field name.

GraphQL union in input object

Problem

Protobuf oneof can be used for both request and response, but GraphQL union can only be used for response (output).

See also:

Solution in this implementation

In this implementation, we use the way mentioned in async-graphql/async-graphql#373 as workaround.

In this way, for the following union:

union Union = A | B

Generate the following input type:

input UnionInput {
  A: A
  B: B
}

This is similar to the way called "Directive" in graphql/graphql-spec#488.

This way has the advantage of supporting unions with overlapping field types, but it has some disadvantages such as the difference between the schema representation (type with multiple optional fields) and the actual processing (union).

It also needs to be checked when actually implementing the object transformation to make sure that multiple fields are not specified at the same time.

Empty objects in GraphQL

Problem

Empty messages and services are supported in Protobuf, but empty objects and queries are not supported in GraphQL.

Protobuf:

message Empty {}

GraphQL:

# ERROR!
type Empty {}

See also:

However, some implementations seem to allow it:

Rust's GraphQL server implementations do not currently allow it:

Solution in this implementation

The workaround for this is to use Boolean no-op fields, as mentioned in graphql/graphql-spec#568 and graphql/graphql-js#937.

For example, the above Protobuf will be converted to the following GraphQL type:

type Empty {
  _noop: Boolean
}

The case where no query is defined is not allowed as well, so we define no-op query to avoid this case as well.

type NoopQuery {
  _noop: Empty!
}

Map in GraphQL

Problem

Types such as Protobuf's map are not supported in GraphQL.

message Map {
  map<string, string> map = 1;
}

Solution in this implementation

This implementation uses Scalar types (input/output is a string but can be used as a parsed type in the application).

type Map {
  map: JSON!
}
"""
A scalar that can represent any JSON value.
"""
scalar JSON

Namespace in GraphQl

Problem

GraphQl does not have a concept like Rust's module or Protobuf's parent message.

So, it is necessary to adjust the name of the GraphQl object to represent the namespace.

Solution in this implementation

In this implementation, we treat a single proto file as the root, and all parent messages from it are added as name prefixes in order from the root side.

In this way, the Inner message of the following Protobuf will be "ParentInner".

message Parent {
  string value = 1;
  message Inner {
    string value = 1;
  }
}
type Parent {
  value: String!
}
type ParentInner {
  value: String!
}

TODO: It might be better if we treat the package name as the root, instead of a single proto file.