GraphQL Codegen

This is an opinionated code-generation tool from GraphQL to Dart/Flutter.

It'll allow you to generate Dart serializers and client helpers with minimal config. The framework makes no assumption on how you structure your fragments or queries, for each operation.graphql the framework will generate a operation.graphq.dart file containing dart classes.

Read more about the tool and motivation at the GraphQL Codegen deep-dive and on how you can structure your flutter apps with the tool on Structure your Flutter GraphQL apps.

The builder relies on json_serializable to generate the actual serializers, so in addition to the two files mentioned above, it'll also generate a operation.graphql.g.dart file.

The framework does not fetch your schema for you, so before you run this, you'll need to add your schema to your project.

Installation

Add graphql_codegen: <current_version> to your dev_dependencies.

The project depends on json_serializable so read more on how to set this up here. It is also a builder, so you'll need to set up build_runner. Read more here.

Basic Usage

To generate dart classes from GraphQL schema, firstly you have to create a schema.graphql file and GraphQL document files.

For instance:

Given schema

# schema.graphql

type Query {
  fetch_person(id: ID!): Person
}

type Person {
  full_name: String!
  nickname: String
  website: URL
  date_of_birth: ISODateTime
  parents: [Person!]
  siblings: [Person!]
  children: [Person!]
}

scalar ISODateTime

scalar URL

and a query

# person.graphql

query FetchPerson($id: ID!) {
  fetch_person(id: $id) {
    name: full_name
  }
}

and then you can generate dart classes with:

$ dart run build_runner build

afterwards, you can parse the result with

// person.dart

import 'person.graphql.dart';

main () {
    final data = fetchDataFromSomewhereMaybeOuterSpace();
    final parsedData = Query$FetchPerson.fromJson(data);
    final name = parsedData.fetchPerson?.name;
    print(name);
}

Using fragments

Fragments are a great tool to re-use queries throughout your app. These are used to create "interfaces" which allow you to easily parse your data around. Given the schema above and the query

# parents_and_children.graphql

fragment PersonSummary on Person {
  full_name
}

query FetchParentsAndChildren {
  fetch_person(id: "1") {
    parents {
      ...PersonSummary
      nickname
    }

    children {
      ...PersonSummary
    }
  }
}

this will allow you to do the following

// parents_and_children.dart

import 'parents_and_children.graphql.dart';

printPerson(FragmentPersonSummary person) {
    print(person.fullName);
}

main () {
    final data = fetchDataFromTheVoid();
    final parsedData = Query$FetchParentsAndChildren.fromJson(data);
    for (final parent in parsedData?.fetchPerson.parents ?? []) {
        printPerson(parent);
        print(parent.dob);
    }
    for (final child in parsedData?.fetchPerson.children ?? []) {
        printPerson(child);
    }
}

The Fragment$PersonSummary is a class on the shape of

...
class Fragment$PersonSummary {
    String get fullName;
}
...

and will be available in the generated .graphql.dart file for the .graphql file containing the fragment.

Inline fragments

Inline fragment spreads work just like fragment spreads with the exception that they don't generate any explicit Fragment$YourFragment classes.

So let's have the schema


type Query {
  account: Account!
}

union Account = 
  | PersonalAccount
  | BusinessAccount


type PersonalAccount {
  personName: String!
}

type BusinessAccount {
  businessName: String!
}

and the query


query FetchAccount {
  account {
    ... on PersonalAccount { personName }
    ... on BusinessAccount { businessName }
  } 
}

the generated classes will allow you to handle the data appropriately with code along the lines of


void printAccount(Query$FetchAccount$account account) {
  if (account is Query$FetchAccount$account$$PersonalAccount) print(account.personName);
  if (account is Query$FetchAccount$account$$BusinessAccount) print(account.businessName);
}

void printQuery(Query$FetchAccount query) {
  printAccount(query.account);  
}

This works but is a long class name! In these cases I usually opt to using named fragments


fragment PersonalAccount on PersonalAccount { personName }

fragment BusinessAccount on BusinessAccount { businessName }

query FetchAccount { 
  account {
    ...BusinessAccount
    ...PersonalAccount
  }
}

which allows you to do the following


void printAccount(Query$FetchAccount$account account) {
  if (account is Fragment$PersonalAccount) print(account.personName);
  if (account is Fragment$BusinessAccount) print(account.businessName);
}

void printQuery(Query$FetchAccount query) {
  printAccount(query.account);  
}

Custom scalars

Out of the box, the standard fragments are supported and mapped to relevant dart types. You can add new mappings for your custom scalars or overwrite existing configurations.

In the schema above, you can see that we have defined the ISODateTime scalar. In this example, it contains a string with an iso formatted date-time string. We would like to map this to darts DateTime type by adding the following configuration to the build.yaml file:

# build.yaml

targets:
  $default:
    builders:
      graphql_codegen:
        options:
          scalars:
            ISODateTime:
              type: DateTime
            JSON:
              type: Map<String, dynamic>

since json_serializable supports parsing DateTime from strings, this is all we need to do.

Assume we want to use a custom date-time class instead (e.g. CustomDateTime) we can add

# build.yaml

targets:
  $default:
    builders:
      graphql_codegen:
        options:
          scalars:
            ISODateTime:
              type: CustomDateTime
              fromJsonFunctionName: customDateTimeFromJson
              toJsonFunctionName: customDateTimeToJson
              import: package:my_app/scalar.dart

and create a scalar.dart file with your converter functions and class.

// custom_date_time.dart
class CustomDateTime {
    final String datetime;

    CustomDateTime(this.datetime);
}

and

// scalar.dart

export 'custom_date_time.dart' show {CustomDateTime};

CustomDateTime customDateTimeFromJson(dynamic data) => CustomDateTime(data as String);
dynamic customDateTimeToJson(CustomDateTime time) => time.datetime;

and now all fields using ISODateTime will be a CustomDateTime instance.

Multiple schemas

To support multiple schemas, the code generator has a concept of scopes. Consider the following configuration:

targets:
  $default:
    builders:
      graphql_codegen:
        options:
          scopes:
            - lib/schema1/**
            - lib/schema2/**

here the generator will perform independent analysis for the GraphQL files matching the relevant scope. E.g., any GraphQL file in the lib/schema1 folder will be built relative to the schema in this folder, ignoring all other files completely.

Clients

Parsing data is all fine and well, but practically not extremely useful. Therefore, we can generate clients to call your API.

Clients can be enabled in the build.yaml file with:

# build.yaml

targets:
  $default:
    builders:
      graphql_codegen:
        options:
          clients:
            - graphql
            - graphql_flutter

Currently, we support two clients:

Client graphql

Once you've set up your `graphql`` client (see pub.dev/packages/graphql), you can use GraphQL Codegen to generate new queries or mutators on the client.

With the query from above:

# person.graphql

query FetchPerson($id: ID!) {
  fetch_person(id: $id) {
    name: full_name
  }
}

we can now access the client:

import 'person.graphql.dart';


main () async {
  final client = GraphQLClient();
  final result = await client.query$FetchPerson(
    Options$Query$FetchPerson(
      variables: Variables$Query$FetchPerson(id: "1"),
    ),
  );
  final parsedData = result.parsedData;
  print(parsedData?.fetchPerson?.name);
}

Cache access

You can also manipulate the cache directly using the generated readFragmentX, writeFragmentX, readQueryX, and writeQueryX methods.

Given the fragment:

fragment PersonSummary on Person {
  name
}

you can update the fragment with


main () {
  // ...
  final person = client.readFragment$PersonSummary(
    idFields: {'__typename': 'Person', 'id': '1'},
  );
  assert(person != null);
  client.writeFragment$PersonSummary(
    idFields: {'__typename': 'Person', 'id': '1'},
    data: person.copyWith(name: 'Kurt'),
  );
}

the query methods work similarly.

Client graphql_flutter

Once you've set up your graphql_flutter`` client (see [pub.dev/packages/graphql_flutter](https://pub.dev/packages/graphql_flutter)), you can use GraphQL Codegen to generate newQueryorMutation` widgets.

With the query from above:

# person.graphql

query FetchPerson($id: ID!) {
  fetch_person(id: $id) {
    name: full_name
  }
}

we can query with the widget

import 'person.graphql.dart';
import 'package:flutter/widgets.dart';

class PersonWidget extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Query$FetchPerson$Widget(
      options: Options$Query$FetchPerson(
        variables: Variables$Query$FetchPerson(id: 'id'),
      ),
      builder: (result, {fetchMore, refetch}) {
        return Text(
          result.parsedData?.fetchPerson?.name ?? '...loading'
        );
      }
    );
  }
}

or the hook

import 'person.graphql.dart';
import 'package:flutter/widgets.dart';
import 'flutter_hooks/flutter_hooks.dart';

class PersonWidget extends HookWidget {

  @override
  Widget build(BuildContext context) {
    final result = useQuery$FetchPerson(
      Options$Query$FetchPerson(
        variables: Variables$Query$FetchPerson(id: 'id'),
      ),
    );
    return Text(result.parsedData?.fetchPerson?.name ?? '...loading');
  }
}

Add typename

By default, the addTypename option is enabled. This'll add the __typename introspection field to every selection set. E.g.,

query Foo {
  bar {
    baz
  }
}

becomes

query Foo {
  bar {
    baz
    __typename
  }
  __typename
}

This ensures the best conditions for caching.

Excluding some selections from adding typename.

Any query, mutation, subscription, or fragment can be excluded from adding the __typename introspection by the addTypenameExcludedPaths option:

Setting

addTypenameExcludedPaths:
  - subscription 

or

addTypenameExcludedPaths:
  - Foo

will both transform


subscription Foo {
  bar {
    baz
  }
}

to


subscription Foo {
  bar { 
    baz
    __typename
  }
}

where

addTypenameExcludedPaths:
  - subscription.bar

or

addTypenameExcludedPaths:
  - subscription.*

or

addTypenameExcludedPaths:
  - **.bar

will transform to


subscription Foo {
  bar { 
    baz
    
  }
  __typename
}

Strip null from input serializers

Some APIs don't allow input fields with a null value but prefer to have no field provided, e.g.,:

{ "foo": null, "bar": "Hello" } // Will fail
{ "bar": "Hello" } // Is preferred

You can strip null values for all input serializers (variables and inputs) with the option

includeIfNullOnInput: false

Change naming separator

The library will generate a lot of serializers and other classes. The class names are a combination of operation, field, and type names. To avoid name collisions, the library will separate each of these names with $.

E.g.,

query Q {
  name
}

might yield the class

class Query$Q$name { ... }

This should work for most, but some other libraries might not support $. Therefore, you can configure the naming separator with the namingSeparator option. E.g., the configuration:

# build.yaml

targets:
  $default:
    builders:
      graphql_codegen:
        options:
          namingSeparator: '___'

will change the above-yielded code to


class Query___Q___name { ... }

Extra keywords

Some APIs will generate fields that are in some way keywords and will break code generation. These might be fields with type names.

You may specify extra keywords with the option

# build.yaml

targets:
  $default:
    builders:
      graphql_codegen:
        options:
          extraKeywords:
            - String

Libraries

builder
graphql_codegen