# GraphLink > GraphQL code generation tool for Dart, Flutter, Java, TypeScript, Kotlin, Swift, and Spring Boot. Open source. MIT License. GraphLink is a command-line tool (`glink`) that reads a GraphQL schema and generates fully type-safe client and server code. Developers define their API once in a `.graphql` schema file and run `glink` to get production-ready, idiomatic code for their target language. ## What GraphLink generates - **Dart / Flutter client**: typed data classes, input classes, enums, toJson/fromJson, a fully typed GraphQL client with named-parameter constructors, and generated HTTP/WebSocket adapters (Dio or package:http, with auto-reconnect WebSocket) — zero boilerplate to set up. - **Java client**: typed POJOs with builder pattern, a GraphQL client where every call returns a concrete typed response — no generics, no TypeReference, no casting at the call site. Ships with generated adapters (Java 11 HttpClient + WebSocket, or OkHttp) and a Jackson/Gson codec so a one-liner `new GraphLinkClient(url)` is all you need. - **TypeScript client**: typed interfaces, enums, input types, a fully typed client with Fetch (default) or Axios HTTP adapter, optional RxJS observables (Angular-friendly), and a default WebSocket adapter for subscriptions with auto-reconnect. Works in Angular, React, Vue, Svelte, and Node/Bun. - **Kotlin client**: typed `data class` types and inputs, enums with `toJson`/`fromJson`, and a coroutine-based GraphQL client using `kotlinx.serialization`. Single-method `GraphLinkClientAdapter` interface (a Kotlin `fun interface`) is all you need for HTTP. Subscriptions via OkHttp WebSocket with auto-reconnect (or `"none"` to bring your own). Works in Android, JVM server projects, and KMP. - **Swift client**: typed `Sendable` structs (with an explicit memberwise `init` and `Identifiable` conformance when an `id: ID!` field is present) and `String, Sendable`-backed enums, both with `toJson`/`fromJson`. An `async`/`await` GraphQL client with no third-party dependency — `URLSessionWebSocketTask` (Foundation) for subscriptions with auto-reconnect (`wsAdapter: "urlsession"`, or `"none"` to bring your own), `AsyncThrowingStream` for subscription results, and an `actor`-based `InMemoryGraphLinkCacheStore` for `@glCache`. Compiles unchanged on Apple platforms and Linux (swift-corelibs-foundation). - **Spring Boot server (Java or Kotlin)**: controllers, service interfaces, repository stubs, and input classes generated from the schema's type definitions and queries. Supports MVC and reactive (WebFlux) mode. Java target: injectable HttpClient/OkHttpClient adapters, plus a reactive client mode (`asyncStyle`: Reactor/RxJava3/Mutiny). Kotlin server target: data-class types, coroutine services, optional `Dispatchers.IO` offloading for blocking service layers. - **Express / Apollo server (TypeScript)**: typed service interfaces, generated Apollo Server resolvers, WebSocket subscriptions via `graphql-ws`, and (optionally) a ready-to-run `createServer(services)` entry point wiring Express + Apollo + the WebSocket server together. `@glSkipOnServer(batch: true)` fields generate per-request DataLoaders instead of Spring's `@BatchMapping`. Same strict-nullability and `toJson()`-boundary-serialization model as the Spring targets. ## Key differentiators - **No generics at the Java call site.** Other Java GraphQL clients require `TypeReference>` on every call. GraphLink generates fully-resolved return types: `client.queries.getUser(id).getUser()` is all you write. - **Built-in caching via schema directives.** Cache behavior is declared in the schema itself using `@glCache(ttl, tags)` and `@glCacheInvalidate(tags)`. Supports tag-based invalidation, partial query caching (per-field TTL), and offline fallback (`staleIfOffline`). - **Zero runtime dependency.** Generated code has no dependency on GraphLink at runtime. Stop using it any time — the generated files keep compiling and working. - **Only what the server needs.** Unlike ferry (which sends the entire schema), GraphLink generates precise queries containing only the requested fields. This is required for Spring Boot's strict schema validation to pass. - **Watch mode.** Run `glink -w` to watch schema files and regenerate automatically on every save. - **Single source of truth.** The schema drives both ends. No duplicate type definitions. No schema drift. Adding a field means editing one file. - **Generic pre-resolver interceptor (`@glIntercept`).** Annotate a root query/mutation/subscription field, a `@glSkipOnServer` schema/batch mapping field, or an entire `type`/`extend type` block with `@glIntercept(tag: "...")` to have the generated controller call a single `runBefore(tag, operation, args, context)` hook before the resolver runs — throw to deny, return to proceed. One generated `GraphLinkInterceptor` interface (plus a `GlInterceptorTag` enum collected from every `tag:` used in the schema) per server project; implement it once as a bean/service. Works across Java Spring (MVC and reactive), Kotlin Spring (blocking and suspend), and Express/Apollo. A batch-mapped field that also declares a real argument must use `@glSkipOnServer(batch: false)` on JVM targets — Spring's `@BatchMapping` handler methods cannot bind a real `@Argument` parameter. - **Two-layer server-side access control.** `@glSkipOnClient` enforces field access on the server at two levels. Level 1 (always active): a `@SchemaMapping` is generated that throws `GraphQLException("Access denied")` — no runtime access regardless of the client. Level 2 (`generateSchema: true`): the field is stripped from the schema Spring Boot serves — invisible in GraphiQL and rejected at validation before execution. Neither level depends on the generated client — any raw GraphQL client or introspection tool is blocked. - **ID/object field-pair pattern.** `@glSkipOnServer` and `@glSkipOnClient` are designed to be used on different fields of the same type. The raw foreign key (`cashierId @glSkipOnClient`) stays on the server for persistence; the resolved object (`cashier @glSkipOnServer(batch: true)`) is loaded by the server and delivered to the client. This eliminates N+1 queries and keeps each side's model clean without any shared representation. - **WebSocket auto-reconnect by default.** Every generated WebSocket adapter — Dart, Java, TypeScript, Kotlin, and Swift — reconnects automatically after a network drop, server restart, or idle timeout. This is where most GraphQL client libraries fall short: they generate the subscription method but leave reconnection to the user. GraphLink handles the full connection lifecycle out of the box, integration-tested across all targets. - **Inline error handling (`@glCaptureErrors`).** Annotate any query or mutation with `@glCaptureErrors` (or set `captureErrors: true` in config) to return a `FullResponse` with `data` and `errors` side by side instead of throwing on GraphQL errors. Dart, Java, TypeScript, Kotlin, and Swift. - **Selective auto-generation (`autoGenerateQueriesFor`).** On large schemas, `autoGenerateQueries: true` generates a client method for every root field — which can be hundreds of methods. `autoGenerateQueriesFor` lets you list exactly which queries, mutations, and subscriptions to auto-generate. Unknown names cause a build error. Supported in all five client targets (Dart, Java, TypeScript, Kotlin, Swift). - **Propagated-arg grouping and limit.** When auto-generating queries, nested field arguments collected from sub-fields are automatically grouped into a synthesized `FieldArgs` input object (the single `fieldArgs` parameter), so method signatures never explode into hundreds of positional parameters. A configurable `autoGenerateQueriesArgumentLimit` (default 200) skips any auto-generated operation whose `fieldArgs` would exceed the cap and prints a `⚠` warning with instructions. Hand-written operations are never affected. - **Oversized fragment cap (`maxFragmentBodySize`).** When `generateAllFieldsFragments: true` is used on a very large schema (e.g. Shopify's public API), individual `_all_fields` fragment bodies can reach 60 KB+, bloating the compiled binary (JVM constant pool, Dart exe) and every network request that references them. `maxFragmentBodySize` (default 8192 chars) silently omits any `_all_fields` fragment whose serialized body exceeds the cap, along with any auto-generated operations that depend on it. Set to `null` to disable the cap and always generate all fragments. Applies to all five client targets (Dart, Java, TypeScript, Kotlin, Swift). - **Barrel file generation.** Dart and TypeScript generators emit a single barrel/index file that re-exports all generated enums, inputs, types, and interfaces. One import covers the entire layer. - **Flutter UI widget generation.** The `flutter` config block generates display widgets for output types and form widgets for input types, with extensive layout and style options. `flutter.sdkVersion` (e.g. "3.47.5", quoted) selects non-deprecated Flutter APIs from 3.35 on; `flutter.designLibrary` (`flutter` | `standalone`) selects `package:flutter/material.dart` or `package:material_ui`. - **Keyword-safe identifiers.** A GraphQL field, input field, argument, or enum value may legally be named after a reserved word in the target language (`default`, `class`, `object`, `val`, ...). GraphLink keeps two names per identifier: the wire name (the original GraphQL token, used in the query string, JSON keys, and variables map — never altered) and the code name (a sanitized identifier used only where the language needs one). On collision an underscore is appended (`default` -> `default_`), with numeric resolution for further clashes (`default_2`). A leading underscore is sanitized too, by moving it to the end (`_link` -> `link_`, and enum values `_ACTIVE` -> `ACTIVE_`): in Dart a leading underscore means private, so this rule is applied across all languages for consistency. The serialization layer bridges the two so the wire format is unchanged. Enum values get the same treatment: because Java/Kotlin couple a constant to its wire string via `valueOf()`/`name()`, GraphLink emits an explicit `switch`/`when` mapping — but only when sanitization occurred, so non-colliding enums keep their clean form. On Spring Boot, a sanitized resolver argument is pinned to the wire name with `@Argument(name = "default")`. TypeScript needs no sanitization (reserved words are legal as object properties and enum members). The whole mechanism is automatic with no configuration. - **Strict server generation with projections (v5.0.0+, breaking change).** Server-generated types and interfaces (Java Spring and Kotlin Spring) now enforce the schema's real nullability by default — a non-null field stays non-null on getters/constructors/setters, instead of the previous always-nullable model. Generated controllers serialize responses via `toJson()` to a `Map` at the controller boundary, and `@glServerLenient` opts a type into all-nullable fields so a resolver can return a partially-populated object. Migration: annotate legacy types with `@glServerLenient` before upgrading if they relied on always-nullable fields. - **Unconditional identifier normalization (v5.0.0+, breaking change).** Every generated identifier — field names, argument names, enum values, type/input/interface/union/enum names — is unconditionally rewritten to the target language's canonical casing (lowerCamelCase fields, SCREAMING_SNAKE Java/Kotlin enum values, PascalCase TypeScript enum values, PascalCase type names). Not opt-in. Because generated identifiers can now diverge from wire names, `toJson`/`fromJson` generation became mandatory on every serializer (previously suppressible via a `generateJsonMethods` flag, now removed). - **Controllers return `Map`/`List` (v5.0.0+, breaking change).** Java Spring, Kotlin Spring, and Express/Apollo controllers now serialize responses via the type's `toJson()` at the controller boundary and return `Map` / `List>` instead of typed objects — service interfaces are unaffected. Java Spring controllers also bind mutation input arguments as `Map`, converting via the input's `fromJson()`. - **Kotlin enum `fromJson` now throws on unknown values (v5.0.0+, breaking change).** Signature changed from `fun fromJson(value: String?): T?` to `fun fromJson(value: String): T` — an unrecognized wire value throws `IllegalArgumentException` instead of returning `null`. ## Supported targets (as of v5.4.0) - Dart client (stable) - Flutter client (stable) - Java client (stable — blocking by default; optional reactive mode via `asyncStyle`: Reactor, RxJava3, Mutiny) - Spring Boot server (stable, MVC and reactive/WebFlux — Java and Kotlin targets, strict-by-default nullability) - TypeScript client (stable — fetch/axios, RxJS observables, WebSocket subscriptions) - Kotlin client (stable — coroutines, kotlinx.serialization, OkHttp WebSocket) - Swift client (stable — async/await, Sendable structs, URLSession/URLSessionWebSocketTask, no third-party dependency) - Express / Apollo server (stable — TypeScript, DataLoader batch mappings, graphql-ws subscriptions) - Go (planned, demand-based) ## Installation Download the prebuilt binary from GitHub Releases: https://github.com/Oualitsen/graphlink/releases/latest Platforms: Linux (x86_64), macOS (ARM64), Windows (x86_64) ## Basic usage ```bash glink # auto-discover glink.json / glink.yaml / glink.yml glink -c glink.json # explicit config path (JSON, YAML, or YML) glink -w # watch mode — regenerate on schema change glink -w --debounce 1000 # watch mode, wait 1000 ms after the last change (default 500) glink -v # print version glink -h # show help ``` ## Configuration (glink.json / glink.yaml) ```json { "schemaPaths": ["schema/*.gql"], "mode": "client", "typeMappings": { "ID": "String", "Float": "Double", "Int": "Integer", "Boolean": "Boolean" }, "unknownScalarType": "Object", "outputDir": "src/main/java/com/example/generated", "clientConfig": { "java": { "packageName": "com.example.generated" } } } ``` ## Caching directives ```graphql type Query { getCar(id: ID!): Car! @glCache(ttl: "5m", tags: ["cars"]) } type Mutation { createCar(input: CreateCarInput!): Car! @glCacheInvalidate(tags: ["cars"]) } ``` ## Documentation pages - Overview: https://graphlink.dev/ - Philosophy (pure codegen, no runtime, schema as source of truth): https://graphlink.dev/philosophy.html - Getting Started (install, first schema, run generator): https://graphlink.dev/getting-started.html - Dart / Flutter client (generated adapters, named-param constructors, queries, mutations, subscriptions, file upload): https://graphlink.dev/dart-client.html - Java client (generated adapters, injectable HttpClient, no-generics API, builder pattern, file upload): https://graphlink.dev/java-client.html - TypeScript client (fetch/axios adapters, RxJS observables, WebSocket subscriptions, Angular/React/Vue examples): https://graphlink.dev/typescript-client.html - Kotlin client (coroutine-based queries/mutations/subscriptions, data classes, kotlinx.serialization, OkHttp WebSocket): https://graphlink.dev/kotlin-client.html - Swift client (async/await queries/mutations/subscriptions, Sendable structs, URLSession/URLSessionWebSocketTask, actor-based cache, no third-party dependency): https://graphlink.dev/swift-client.html - Spring Boot server (MVC + reactive/WebFlux, Java and Kotlin targets, strict-by-default generation, Map-boundary controllers, service interfaces, file upload, security context propagation): https://graphlink.dev/spring-server.html - Express / Apollo server (TypeScript service interfaces, generated resolvers, DataLoader batch mappings, graphql-ws subscriptions, createServer entry point, @glIntercept, @glValidate): https://graphlink.dev/express-apollo.html - Caching (@glCache, @glCacheInvalidate, tag invalidation, staleIfOffline): https://graphlink.dev/caching.html - Directives (@glMapsTo, @glMapField, @glCache, @glCacheInvalidate, @glSkipOnServer forward mappings, @glServerLenient, @glInjectContext, @glIntercept, @deprecated, and more): https://graphlink.dev/directives.html - "@glSkipOnServer & @glSkipOnClient" (two-layer server-side access control, schema stripping, @SchemaMapping, @BatchMapping, mapTo, forward mappings, ID/object field-pair pattern for N+1-safe loading): https://graphlink.dev/skip-directives.html - Configuration (all config keys for Dart, Java, TypeScript, Kotlin, Swift, Spring; autoGenerateQueriesFor for selective operation generation; autoGenerateQueriesArgumentLimit for propagated-arg cap; maxFragmentBodySize to skip oversized _all_fields fragments and their dependent auto-queries; reactive; useSpringSecurity; typeMappings; unknownScalarType fallback for unmapped custom scalars; CLI flags; YAML config): https://graphlink.dev/configuration.html ## Links - Website: https://graphlink.dev/ - Docs: https://graphlink.dev/ - GitHub: https://github.com/Oualitsen/graphlink - pub.dev: https://pub.dev/packages/graphlink - Issues: https://github.com/Oualitsen/graphlink/issues - Releases: https://github.com/Oualitsen/graphlink/releases --- # GraphLink
## You already wrote the schema. ### Why are you writing the interface again? And the class? GraphLink is the only generator that treats your `.graphql` file as the *actual* source of truth — not documentation you also hand-copy into two more languages, every time it changes, forever.
> Your `.graphql` schema compiles to a fully typed client and server — for Dart, Flutter, Java, TypeScript, Kotlin, Swift, Spring Boot, and Express/Apollo — in milliseconds. [![pub.dev](https://img.shields.io/pub/v/graphlink?label=pub.dev)](https://pub.dev/packages/graphlink) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Oualitsen/graphlink/blob/main/LICENSE) [![GitHub Releases](https://img.shields.io/github/v/release/Oualitsen/graphlink)](https://github.com/Oualitsen/graphlink/releases/latest) **No runtime. No boilerplate. No schema drift.** ---
### What stops being your problem 1. **Your database falling over from N+1 queries.** A clean-looking resolver fires 500 tiny queries for one list view — invisible in dev, discovered in production. Declare `@glSkipOnServer(batch: true)` once; GraphLink generates the `DataLoader` / `@BatchMapping` for you. 2. **Three hand-maintained copies of every type.** Schema, frontend interface, backend class — same shape, written by hand, three times, drifting apart within a sprint. One schema, six typed targets, regenerated in milliseconds. 3. **Untyped response casting.** `TypeReference>>` and its friends. GraphLink returns fully-resolved typed objects on every target — no casting, no generics, no guessing at the shape. 4. **Auth checks copy-pasted into every resolver** — and forgotten on the one you added last week. `@glIntercept` runs one generated hook before any annotated field executes, so there's nothing to forget. 5. **Cache logic that requires a PhD to operate.** Apollo Client: `typePolicies` per type, hand-written `merge` functions for pagination, and `cache.evict`/`cache.modify` calls keyed to the *exact* normalized field-and-args cache identifier — get one character wrong and stale data just sits there, silently, forever. We ran the numbers, peer-reviewed and everything: **The Apollo way** ```text { .gl-cache-bad } cacheKey ≜ ROOT_QUERY.field( ∮ Σᵢ₌₀ⁿ argsᵢ · e^(iπ·keyArgs) dΩ ) ⊕ ∬∬ merge(existing, incoming, {args, isReference, readField, storeFieldName}) d(typePolicies)² − ∂³(sanity) / ∂cache.modify ∂cache.evict ∂mergeFunction + lim_{x→∞} StackOverflow(x) ≈ NaN (converges to "it depends", p < 0.05, campfire required) ``` Checks out. Nobody on the team understands it. Apollo's a genuinely great library — this part just needs a PhD. --- **The GraphLink way** ```graphql { .gl-cache-good } type Query { # cached for 5 minutes, tagged "vehicles" — nothing to configure client-side getVehicle(id: ID!): Vehicle! @glCache(ttl: "5m", tags: ["vehicles"]) } type Mutation { # on success, every "vehicles"-tagged cache entry is evicted automatically addVehicle(input: AddVehicleInput!): Vehicle! @glCacheInvalidate(tags: ["vehicles"]) } ``` 6. **Getting locked into a runtime.** Generated code has zero dependency on GraphLink. Stop using it tomorrow and everything still compiles.
--- ## One schema. Every client. Every language. Write your API once in GraphQL. Run `glink`. Every target regenerates in milliseconds. ``` schema/*.graphql │ ├── lib/generated/ Flutter / Dart client ├── src/main/java/com/app/generated/ Java client ├── src/generated/ TypeScript client ├── src/main/kotlin/com/app/generated/ Kotlin client └── src/main/java/com/app/generated/ Spring Boot server ``` Add a field to the schema — run `glink` — it appears in every client, every language, immediately typed. No hand-written DTOs. No copy-pasted response classes. No drift between frontend and backend. === "Dart / Flutter" ```dart final res = await client.queries.getVehicle(id: "v-1"); print("${res.getVehicle.brand} ${res.getVehicle.model}"); ``` === "Java" ```java Vehicle v = client.queries.getVehicle("v-1").getVehicle(); System.out.println(v.getBrand() + " " + v.getModel()); ``` === "TypeScript" ```typescript const res = await client.queries.getVehicle("v-1"); console.log(`${res.getVehicle.brand} ${res.getVehicle.model}`); ``` === "Kotlin" ```kotlin val v = client.queries.getVehicle("v-1").getVehicle println("${v.brand} ${v.model}") ``` The call site is clean in every language. One schema. Zero boilerplate. --- ## Before GraphLink, After GraphLink Every GraphQL client library sells you the same promise: write a schema, get a client. What they don't show you is what "client" actually meant before code generation caught up to that promise — the hand-written DTO, the casts, the generics, kept in sync with the server by hand, in every language you ship. === "Java"
**Without GraphLink** ```java GraphQLResponse> response = client.query(new SimpleGraphQLRequest<>( "query getVehicle($id: ID!) { getVehicle(id: $id) { id brand year } }", Map.of("id", "42"), new TypeReference>>() {} )); @SuppressWarnings("unchecked") Map vehicleMap = (Map) response.getData().get("getVehicle"); String brand = (String) vehicleMap.get("brand"); Integer year = ((Number) vehicleMap.get("year")).intValue(); ```
**With GraphLink** ```java Vehicle v = client.queries.getVehicle("42").getVehicle(); System.out.println(v.getBrand() + " " + v.getYear()); ```
=== "Dart"
**Without GraphLink** ```dart final response = await http.post(uri, body: jsonEncode({ 'query': query, 'variables': {'id': id}, })); final json = jsonDecode(response.body) as Map; final vehicle = json['data']['getVehicle'] as Map; final brand = vehicle['brand'] as String; final year = vehicle['year'] as int; // Hope nobody renamed a field on the server last sprint. ```
**With GraphLink** ```dart final res = await client.queries.getVehicle(id: '42'); print('${res.getVehicle.brand} ${res.getVehicle.year}'); ```
=== "Kotlin"
**Without GraphLink** ```kotlin val response = httpClient.post(url) { setBody(payload) } val json = Json.parseToJsonElement(response.bodyAsText()).jsonObject val vehicleJson = json["data"]!!.jsonObject["getVehicle"]!!.jsonObject val vehicle = Vehicle( id = vehicleJson["id"]!!.jsonPrimitive.content, brand = vehicleJson["brand"]!!.jsonPrimitive.content, year = vehicleJson["year"]!!.jsonPrimitive.int ) // Hand-written data class, hand-written parsing, kept in sync by hand. ```
**With GraphLink** ```kotlin val v = client.queries.getVehicle("42").getVehicle println("${v.brand} ${v.year}") ```
=== "TypeScript"
**Without GraphLink** ```typescript interface Vehicle { id: string; brand: string; year: number; } // hand-written, drifts from schema const res = await fetch(url, { method: 'POST', body: JSON.stringify({ query, variables: { id } }) }); const json = await res.json(); const vehicle = json.data.getVehicle as Vehicle; // "as" — TS trusts you, the server doesn't have to console.log(vehicle.brand, vehicle.year); ```
**With GraphLink** ```typescript const res = await client.queries.getVehicle("42"); console.log(res.getVehicle.brand, res.getVehicle.year); ```
It's not only the typed fetch. The same gap shows up in **subscriptions** (hand-rolled reconnect/backoff vs. an adapter that reconnects forever, out of the box) and in **caching** (a bespoke TTL/tag cache re-implemented per language vs. `@glCache(ttl: "2m", tags: ["vehicles"])` declared once in the schema). Multiply all of it by every query, every mutation, every language your stack touches, and the real cost of "no codegen" shows up: hundreds of hand-written DTOs and a schema that quietly drifts from the code that's supposed to implement it. --- ## Why GraphLink? ### 1. Cache control lives in the schema Annotate any query or mutation directly in your `.graphql` file. The generated client handles TTL, tag-based invalidation, partial query caching, and offline fallback automatically — across every target language. ```graphql type Query { getVehicle(id: ID!): Vehicle! @glCache(ttl: "2m", tags: ["vehicles"]) listVehicles: [Vehicle!]! @glCache(ttl: "1m", tags: ["vehicles"]) } type Mutation { addVehicle(input: AddVehicleInput!): Vehicle! @glCacheInvalidate(tags: ["vehicles"]) } ``` The second call is served from cache. `addVehicle` evicts the `"vehicles"` tag automatically. No cache layer to wire up. No manual invalidation logic. The schema says it; the generated client does it. --- ### 2. Subscriptions — with auto-reconnect out of the box This is where most GraphQL client libraries fall short: they generate a WebSocket subscription method but leave reconnection entirely to you. Network drops, server restarts, idle timeouts — your app silently stops receiving events. GraphLink generates a default WebSocket adapter for every target — **with auto-reconnect enabled by default**. The connection drops, it comes back, your `Flow` / `Stream` / `Observable` keeps emitting. No reconnection logic to write. No silent failures. === "Kotlin" ```kotlin scope.launch { client.subscriptions.userCreated().collect { event -> println("New user: ${event.userCreated.name}") } } ``` === "Dart" ```dart client.subscriptions.onUserCreated().listen((event) { print("New user: ${event.userCreated.name}"); }); ``` === "TypeScript" ```typescript client.subscriptions.userCreated().subscribe(event => { console.log("New user:", event.userCreated.name); }); ``` The generated adapter speaks the [graphql-ws](https://github.com/enisdenjo/graphql-ws) protocol and handles the full connection lifecycle — `connection_init`, `connection_ack`, `subscribe`, `ping/pong`, and reconnect — all covered by integration tests. --- ### 3. The server is generated too GraphLink isn't only a client generator. Switch to `mode: server` and point it at your Spring Boot project. It generates controllers, service interfaces, input classes, and DTOs — wired up to the schema, ready to implement. ``` schema/*.graphql → @SchemaMapping controllers Service interfaces to implement Input classes with null-safety Type DTOs ``` You implement the business logic. GraphLink handles the plumbing. --- ### 4. Generated code you own completely The output is ordinary Dart, Java, TypeScript, Kotlin, or Swift — no runtime dependency on GraphLink whatsoever. Stop using it tomorrow and everything keeps compiling. Commit the output to your repo, vendor it, ship it. It's just code. --- ## In production Measured in [Optidialysis](https://www.optidialysis.com/) — a multi-tenant dialysis clinic management platform. **72% of the Spring Boot backend — generated.** Controllers, service interfaces, DTOs, input classes, enums — all from one `.graphql` file. Only **135 files (~11.8k lines)** written by hand across the entire backend. | What | Generated | |---|---| | Spring Boot files | **72%** | | Spring Boot lines | **64%** | | Flutter codebase | **21.5%** | Every generated file compiles, ships, and runs with zero runtime dependency on GraphLink. --- ## Supported targets **Stable:** Dart · Flutter · Java · Spring Boot · TypeScript · Kotlin · Swift · Express / Apollo **Planned:** Go --- ## Installation Download the single self-contained binary — no JVM, no package manager required. === "macOS (ARM)" ```bash curl -fsSL https://github.com/Oualitsen/graphlink/releases/latest/download/glink-macos-arm64 -o glink chmod +x glink && sudo mv glink /usr/local/bin/glink ``` === "macOS (x64)" ```bash curl -fsSL https://github.com/Oualitsen/graphlink/releases/latest/download/glink-macos-x64 -o glink chmod +x glink && sudo mv glink /usr/local/bin/glink ``` === "Linux (x64)" ```bash curl -fsSL https://github.com/Oualitsen/graphlink/releases/latest/download/glink-linux-x64 -o glink chmod +x glink && sudo mv glink /usr/local/bin/glink ``` === "Windows" Download [`glink-windows-x64.exe`](https://github.com/Oualitsen/graphlink/releases/latest) and add it to your PATH. === "Dart / Flutter (pub)" ```bash flutter pub add --dev graphlink # or dart pub add --dev graphlink ``` === "Docker" ```bash docker run --rm -v "$PWD":/workspace -w /workspace \ oualitsen/graphlink:latest -c glink.json ``` Multi-arch (`amd64`/`arm64`) image: [`oualitsen/graphlink`](https://hub.docker.com/r/oualitsen/graphlink). No JVM, no local binary — good fit for CI. --- ## Quick Start ### 1. Write your schema ```graphql type Vehicle { id: ID! brand: String! model: String! year: Int! fuelType: FuelType! } enum FuelType { GASOLINE DIESEL ELECTRIC HYBRID } input AddVehicleInput { brand: String! model: String! year: Int! fuelType: FuelType! } type Query { getVehicle(id: ID!): Vehicle! @glCache(ttl: "2m", tags: ["vehicles"]) listVehicles: [Vehicle!]! @glCache(ttl: "1m", tags: ["vehicles"]) } type Mutation { addVehicle(input: AddVehicleInput!): Vehicle! @glCacheInvalidate(tags: ["vehicles"]) } ``` ### 2. Configure Pick your target and drop a `glink.yaml` next to your schema: === "Dart / Flutter" ```yaml schemaPaths: - lib/**/*.graphql mode: client typeMappings: ID: String Float: double Int: int Boolean: bool outputDir: lib/generated clientConfig: dart: packageName: my_app ``` === "Java" ```yaml schemaPaths: - schema/*.graphql mode: client typeMappings: ID: String Float: Double Int: Integer Boolean: Boolean outputDir: src/main/java/com/example/generated clientConfig: java: packageName: com.example.generated ``` === "TypeScript" ```yaml schemaPaths: - schema/*.graphql mode: client typeMappings: ID: string Float: number Int: number Boolean: boolean outputDir: src/generated clientConfig: typescript: httpAdapter: fetch ``` === "Kotlin" ```yaml schemaPaths: - schema/*.graphql mode: client typeMappings: ID: String Float: Double Int: Int Boolean: Boolean outputDir: src/main/kotlin/com/example/generated clientConfig: kotlin: packageName: com.example.generated ``` === "Spring Boot" ```yaml schemaPaths: - src/main/resources/graphql/*.graphqls mode: server typeMappings: ID: String Float: Double Int: Integer Boolean: Boolean outputDir: src/main/java/com/example/generated serverConfig: spring: basePackage: com.example.generated ``` === "Express / Apollo" ```yaml schemaPaths: - schema/*.graphql mode: server typeMappings: ID: string Float: number Int: number Boolean: boolean outputDir: src/generated serverConfig: expressApollo: port: 4000 graphqlPath: /graphql ``` ### 3. Generate ```bash glink # auto-discovers glink.yaml / glink.json / glink.yml glink -w # watch mode — regenerate on every save ``` → See [Getting Started](getting-started.md) for a complete walkthrough per language. --- ## FAQ ??? "Does the generated code have a runtime dependency on GraphLink?" None. If you stop using GraphLink tomorrow, every generated file continues to compile and work exactly as before. The output is ordinary Dart, Java, TypeScript, Kotlin, or Swift — you own it completely. ??? "What happens when I add a field to the schema?" Run `glink` (or let `glink -w` pick it up automatically). All affected files are regenerated and the new field is immediately available as a typed property in every target. One file to edit — GraphLink handles the rest. ??? "How does the built-in caching work?" Cache behaviour is declared in the schema using two directives. `@glCache(ttl: "5m", tags: ["cars"])` caches a query result for 5 minutes under the tag `"cars"`. `@glCacheInvalidate(tags: ["cars"])` on a mutation evicts all entries tagged `"cars"` when the mutation succeeds. Individual fields inside a compound query can each carry their own TTL — if one tag is invalidated, the others stay warm. See [Caching](caching.md) for the full reference. ??? "Is GraphLink production-ready?" Yes. In one production deployment (multi-tenant SaaS, dialysis clinic management), 72% of Spring Boot files and 64% of lines are generated — only 135 files written by hand across the entire backend. On the Flutter side, 21.5% of the codebase is generated, covering all DTOs, input classes, enums, and GraphQL client wiring. ??? "Can I use GraphLink with an existing project?" Yes. Point `schemaPaths` at your existing `.graphql` files and set `outputDir` to wherever you want the generated files. GraphLink does not touch any of your existing source files. ??? "Does it work for full-stack monorepos?" Yes — this is the primary use case. Run `glink` with different config files (or different `outputDir` values) to generate the Flutter client, the Java client, and the Spring Boot server from the same schema. One schema, multiple configs, all in sync. --- ## Documentation
- **[Philosophy](philosophy.md)** — Why pure code generation? Why no runtime abstractions? - **[Getting Started](getting-started.md)** — Zero to generated code in 5 minutes. - **[Dart / Flutter Client](dart-client.md)** — Typed queries, mutations, subscriptions. Adapter pattern. - **[Java Client](java-client.md)** — No generics. No casting. Builder pattern on all inputs. - **[TypeScript Client](typescript-client.md)** — Typed client for Angular, React, Vue, and Node. - **[Kotlin Client](kotlin-client.md)** — Coroutine-based client with data classes and kotlinx.serialization. - **[Swift Client](swift-client.md)** — async/await client with Sendable structs, no third-party dependency. - **[Spring Boot](spring-server.md)** — Generated controllers, service interfaces, types, inputs. - **[Express / Apollo](express-apollo.md)** — Generated resolvers, service interfaces, DataLoader batch mappings. - **[Caching](caching.md)** — `@glCache` and `@glCacheInvalidate`. Tag-based invalidation. - **[Directives](directives.md)** — Complete reference for all GraphLink directives. - **[Configuration](configuration.md)** — Every `glink.json` / `glink.yaml` option explained. - **[AI Agents](ai-agents.md)** — Ready-to-copy instructions for Claude Code, Gemini CLI, and others.
--- **GraphLink** — MIT License — [graphlink.dev](https://graphlink.dev) | | | |---|---| | **Source** | [github.com/Oualitsen/graphlink](https://github.com/Oualitsen/graphlink) | | **Package** | [pub.dev/packages/graphlink](https://pub.dev/packages/graphlink) | | **Releases** | [github.com/Oualitsen/graphlink/releases](https://github.com/Oualitsen/graphlink/releases) | | **Changelog** | [CHANGELOG.md](https://github.com/Oualitsen/graphlink/blob/main/CHANGELOG.md) | | **Bug reports** | [github.com/Oualitsen/graphlink/issues](https://github.com/Oualitsen/graphlink/issues) | | **Discussions** | [github.com/Oualitsen/graphlink/discussions](https://github.com/Oualitsen/graphlink/discussions) | | **Author** | [github.com/Oualitsen](https://github.com/Oualitsen) | --- # The GraphLink Philosophy Pure code generation. No runtime abstractions. Only what the server needs. ## What pure code generation means GraphLink is a code generator, not a runtime library. When you run `glink`, it reads your schema and writes ordinary Dart, Java, TypeScript, Kotlin, or Swift source files. Those files have zero dependency on GraphLink itself — no base classes to extend, no interfaces to implement, no runtime to ship. Every generated class is plain code in the target language. A generated `Vehicle` class in Dart is just a Dart class with fields, a constructor, `fromJson`, and `toJson`. A generated `Vehicle.java` is just a POJO with a builder. A generated Kotlin `Vehicle` is a `data class`, and a generated Swift `Vehicle` is a `Sendable` struct. You could hand-write these classes yourself; GraphLink simply writes them faster and keeps them in sync with your schema. **Delete GraphLink from your project tomorrow and everything still compiles.** There is nothing to uninstall, no peer dependency to satisfy, no API surface that could be deprecated. The generated code is yours. !!! success "The practical implication" You can upgrade GraphLink independently of your app. You can audit every line of code that runs in production. You can add methods to generated classes without fighting a framework. You own the output. ## Only what the server needs This is one of the most important design decisions in GraphLink, and it directly affects Spring Boot compatibility. Many GraphQL clients (such as ferry in Dart) serialize the *entire schema document* and send it with every request. This includes type definitions, directives, fragment definitions, comments, and whitespace. On the wire this looks like: ```json title="What other clients send" { "query": "fragment _all_fields_Vehicle on Vehicle { id brand model year fuelType ownerId } query getVehicle($id: ID!) { getVehicle(id: $id) { ... _all_fields_Vehicle } }", "variables": { "id": "42" } } ``` Spring Boot's `graphql-java` validates the query document on every request. If the client sends fragment definitions for types or fields that the server doesn't know about — or just sends unnecessary bulk — the server may reject the request or log validation warnings. GraphLink generates minimal, precise query strings. The client sends exactly the fields needed for the response type, no fragment boilerplate, no extra definitions: ```json title="What GraphLink sends" { "query": "query getVehicle($id: ID!) { getVehicle(id: $id) { id brand model year fuelType ownerId } }", "variables": { "id": "42" } } ``` This is a significant operational difference. It means GraphLink clients work out-of-the-box with strict Spring Boot GraphQL servers, with AWS AppSync, and with any other server that validates incoming documents carefully. ## The schema is the single source of truth In a typical project without code generation, the same concept is expressed in three places: the GraphQL schema, the data transfer objects (DTOs) in the server language, and the model classes in the client language. When the schema changes, all three must be updated manually. When they get out of sync — which they always do — you get runtime errors, not compile-time errors. GraphLink collapses all three into one. You edit the schema. You re-run the generator. Every DTO, every model class, every query string, every serializer is immediately regenerated and consistent. Schema drift becomes impossible. This also means code review is simpler: a schema change in a pull request implies a corresponding regeneration of client and server code. Reviewers can read the schema diff and know immediately what changed. ## No generics at the Java call site Most Java GraphQL clients force you to carry type information through generics. A typical query call looks like: ```java title="Other clients — the generic soup" // What you write with most clients GraphQLResponse> response = client.query(new SimpleGraphQLRequest<>( QUERY_STRING, variables, new TypeReference>>() {} )); Vehicle vehicle = objectMapper.convertValue( response.getData().get("getVehicle"), Vehicle.class ); ``` ```java title="GraphLink — no generics, no casting" // What you write with GraphLink GetVehicleResponse res = client.queries.getVehicle("42"); System.out.println(res.getGetVehicle().getBrand()); ``` GraphLink generates a specific `getVehicle(String id)` method that returns a specific `GetVehicleResponse` type. The compiler knows the exact return type. There are no anonymous `TypeReference` subclasses, no unchecked casts, no `Map` to navigate manually. ## Keyword-safe identifiers GraphQL has its own set of reserved words, and they rarely overlap with those of a target language. A field, input field, argument, or enum value may legally be named `default`, `class`, `object`, or `val` in your schema — all perfectly valid GraphQL, all reserved words in Dart, Java, or Kotlin. A naive generator would emit code that does not compile. GraphLink keeps two names for every identifier: - The **wire name** — the original GraphQL token. It is what travels in the query string, the JSON keys, and the variables map. It is never altered. - The **code name** — a sanitized identifier used only where the target language needs one. When the wire name collides with a reserved word, GraphLink appends an underscore (`default` → `default_`), resolving further collisions numerically (`default_2`, `default_3`). A **leading underscore** is sanitized the same way. In Dart a leading underscore means *private*, so a field named `_link` would be inaccessible and break the generated code. GraphLink moves the underscore to the end (`_link` → `link_`). The same applies to enum values (`_ACTIVE` → `ACTIVE_`). For consistency this rule is applied across **all** target languages, not just Dart — the wire name still keeps its leading underscore. The serialization layer bridges the two, so the wire format stays exactly as your schema declares it: ```graphql title="Schema — a field named after a Dart/Java/Kotlin keyword" type Settings { default: String theme: String } ``` ```dart title="Dart — identifier sanitized, JSON key untouched" class Settings { final String? default_; final String? theme; Map toJson() => { 'default': default_, // wire key stays "default" 'theme': theme, }; Settings.fromJson(Map json) : default_ = json['default'] as String?, theme = json['theme'] as String?; } ``` Enum values get the same treatment. Because Java and Kotlin couple a constant's name to its wire string via `valueOf()` / `name()`, GraphLink emits an explicit `switch` / `when` mapping for the sanitized constants — but only when sanitization actually happened, so enums without keyword collisions keep their clean `name()` / `valueOf()` form. On the Spring Boot server, a sanitized resolver argument is pinned back to the wire name with `@Argument(name = "default")` so request binding still works. TypeScript needs none of this: its reserved words are legal as object property names and enum members, so identifiers are emitted verbatim. The whole mechanism is automatic — there is no configuration and nothing to opt into. Name a field after a keyword and the generated code simply compiles. ## Framework agnostic GraphLink generates the logic — the query strings, the serialization, the client wiring — but it never dictates how you make HTTP requests. You provide the transport as a simple function: - In Dart: `Future Function(String payload)` - In Java: a `@FunctionalInterface` that takes a JSON string and returns a JSON string - In TypeScript: `(payload: string) => Promise` - In Kotlin: a `fun interface` with a single `suspend` method - In Swift: a `GraphLinkClientAdapter` closure typealias This means you can use `http`, `dio`, `fetch`, `OkHttp`, `URLSession`, Spring's `WebClient`, a mock, or anything else. The generated client does not import any HTTP library. You swap the transport at construction time. This makes testing trivial — pass a function that returns a hardcoded JSON string and you have a complete mock client. ## How it compares GraphLink occupies a distinct position in the GraphQL tooling landscape. Here is a direct comparison with popular alternatives: | Feature | GraphLink | ferry (Dart) | Apollo (JS/Kotlin) | Manual code | |---|---|---|---|---| | Runtime dependency in app | **None** | Yes (ferry, gql) | Yes (Apollo runtime) | None | | Sends whole schema on request | **No** | Yes | Partial | No | | Generics at Java call site | **No** | N/A | Yes | Yes | | Server-side generation | **Yes** | No | Partial | Manual | | Java client support | **Yes** | No | Kotlin only | Manual | | Caching directives in schema | **Yes** | No | No | No | | Partial query caching | **Yes** | No | No | No | | Spring Boot controller gen | **Yes** | No | No | Manual | --- # Getting Started From zero to generated code in under 5 minutes. ## Step 1 — Download the CLI The GraphLink CLI is distributed as a single self-contained binary called `glink`. No runtime, no package manager, no JVM required. === "Linux / macOS" ```bash # Download the latest release curl -fsSL https://github.com/Oualitsen/graphlink/releases/latest/download/glink-linux-x64 -o glink # Make it executable chmod +x glink # Move to your PATH sudo mv glink /usr/local/bin/glink # Verify glink --version ``` === "Windows" ```powershell # Download via PowerShell Invoke-WebRequest ` -Uri "https://github.com/Oualitsen/graphlink/releases/latest/download/glink-windows-x64.exe" ` -OutFile "glink.exe" # Add to PATH or run from current directory .\glink.exe --version ``` === "Docker" A multi-arch (`linux/amd64` + `linux/arm64`) image is published to Docker Hub as [`oualitsen/graphlink`](https://hub.docker.com/r/oualitsen/graphlink) — no local binary, no PATH setup: ```bash # Mount your project directory and run glink against it docker run --rm -v "$PWD":/workspace -w /workspace \ oualitsen/graphlink:latest -c glink.json # Pin to a specific version instead of "latest" docker run --rm -v "$PWD":/workspace -w /workspace \ oualitsen/graphlink:5.0.0 --version ``` The container's entrypoint is `glink` itself — everything after the image name is passed straight through as CLI arguments (`-c`, `-w`, `--version`, `--help`, ...). Useful for CI pipelines that shouldn't depend on a language runtime just to run the generator. === "Manual" ``` Visit: https://github.com/Oualitsen/graphlink/releases/latest Download the binary for your platform: glink-linux-x64 — Linux (x86_64) glink-linux-arm64 — Linux (ARM64 / Raspberry Pi) glink-macos-x64 — macOS (Intel) glink-macos-arm64 — macOS (Apple Silicon) glink-windows-x64.exe — Windows (x86_64) Place the binary somewhere on your PATH and make it executable. ``` ## Step 2 — Write your schema Create a `schema/` directory and save your GraphQL schema as one or more `.graphql` or `.gql` files. Here is the schema used throughout this documentation: ```graphql title="schema/schema.graphql" # An enum — GraphLink generates serialization in both directions enum FuelType { GASOLINE DIESEL ELECTRIC HYBRID } # A type — generates a model class with fromJson/toJson (Dart) or builder + getters (Java) type Person { id: ID! # ID maps to String in both Dart and Java name: String! email: String! vehicles: [Vehicle!]! # nested list — fully typed } type Vehicle { id: ID! brand: String! model: String! year: Int! fuelType: FuelType! ownerId: ID # nullable — String? in Dart, @Nullable String in Java } # Input types — generate immutable input classes with builders input AddPersonInput { name: String! email: String! } input AddVehicleInput { brand: String! model: String! year: Int! fuelType: FuelType! ownerId: ID } # Queries — @glCache caches the result; ttl as duration string; tags for group invalidation type Query { getPerson(id: ID!): Person getVehicle(id: ID!): Vehicle! @glCache(ttl: "2m", tags: ["vehicles"]) listVehicles: [Vehicle!]! @glCache(ttl: "1m", tags: ["vehicles"]) } # Mutations — @glCacheInvalidate evicts all entries with matching tags on success type Mutation { addPerson(input: AddPersonInput!): Person! addVehicle(input: AddVehicleInput!): Vehicle! @glCacheInvalidate(tags: ["vehicles"]) } # Subscriptions — backed by a WebSocket connection type Subscription { vehicleAdded: Vehicle! } ``` !!! info "Splitting a type's body across files (v5.0.0+)" A `type`, `interface`, or `input` can now be declared with no body at all (`type Query { }`) as long as a later `extend` block somewhere in `schemaPaths` supplies its fields. GraphLink raises a validation error if a bodyless definition is never extended. This is useful for schemas split across multiple files where the root type is declared in one file and extended per-feature in others. ## Step 3 — Configure the generator Create a config file in your project root. GraphLink accepts **JSON** (`.json`), **YAML** (`.yaml`), or **YML** (`.yml`). Name it `glink.json` / `glink.yaml` / `glink.yml` and the `-c` flag becomes optional — `glink` finds it automatically. === "Dart / Flutter" === "JSON" ```json title="glink.json" { "schemaPaths": ["schema/*.graphql"], "mode": "client", "typeMappings": { "ID": "String", "String": "String", "Float": "double", "Int": "int", "Boolean": "bool", "Null": "null" }, "outputDir": "lib/generated", "clientConfig": { "dart": { "packageName": "my_app", "generateAllFieldsFragments": true, "autoGenerateQueries": true, "nullableFieldsRequired": false, "immutableInputFields": true, "immutableTypeFields": true } } } ``` === "YAML" ```yaml title="glink.yaml" schemaPaths: - schema/*.graphql mode: client typeMappings: ID: String String: String Float: double Int: int Boolean: bool Null: null outputDir: lib/generated clientConfig: dart: packageName: my_app generateAllFieldsFragments: true autoGenerateQueries: true nullableFieldsRequired: false immutableInputFields: true immutableTypeFields: true ``` === "Java client" === "JSON" ```json title="glink.json" { "schemaPaths": ["schema/*.graphql"], "mode": "client", "typeMappings": { "ID": "String", "String": "String", "Float": "Double", "Int": "Integer", "Boolean": "Boolean", "Null": "null" }, "outputDir": "src/main/java/com/example/generated", "clientConfig": { "java": { "packageName": "com.example.generated", "generateAllFieldsFragments": true, "autoGenerateQueries": true, "nullableFieldsRequired": false, "immutableInputFields": true, "immutableTypeFields": true } } } ``` === "YAML" ```yaml title="glink.yaml" schemaPaths: - schema/*.graphql mode: client typeMappings: ID: String String: String Float: Double Int: Integer Boolean: Boolean Null: null outputDir: src/main/java/com/example/generated clientConfig: java: packageName: com.example.generated generateAllFieldsFragments: true autoGenerateQueries: true nullableFieldsRequired: false immutableInputFields: true immutableTypeFields: true ``` === "Spring Boot" === "JSON" ```json title="glink.json" { "schemaPaths": ["schema/*.graphql"], "mode": "server", "typeMappings": { "ID": "String", "String": "String", "Float": "Double", "Int": "Integer", "Boolean": "Boolean", "Null": "null" }, "outputDir": "src/main/java/com/example/generated", "serverConfig": { "spring": { "basePackage": "com.example.generated", "generateControllers": true, "generateRepositories": false, "immutableInputFields": true, "immutableTypeFields": false } } } ``` === "YAML" ```yaml title="glink.yaml" schemaPaths: - schema/*.graphql mode: server typeMappings: ID: String String: String Float: Double Int: Integer Boolean: Boolean Null: null outputDir: src/main/java/com/example/generated serverConfig: spring: basePackage: com.example.generated generateControllers: true generateRepositories: false immutableInputFields: true immutableTypeFields: false ``` ### Key configuration options explained | Key | Description | |---|---| | `schemaPaths` | Glob patterns for schema files. You can split the schema across multiple files. | | `mode` | `"client"` generates client code; `"server"` generates Spring Boot scaffolding. | | `typeMappings` | Maps GraphQL scalar types to language types. Add entries for custom scalars. | | `outputDir` | Where to write generated files. Existing files are overwritten. | | `generateAllFieldsFragments` | Generates a `_all_fields_TypeName` fragment per type, used by `autoGenerateQueries`. | | `autoGenerateQueries` | Generates query strings for every Query/Mutation/Subscription field automatically. | | `immutableInputFields` | Input class fields are `final` (Dart) or `final` (Java). Inputs become builder-only. | | `immutableTypeFields` | Response type fields are final. For Spring server, set to `false` so Spring can set fields. | | `generateControllers` | Spring only. Generates `@Controller` classes wired with `@QueryMapping` etc. | | `generateRepositories` | Spring only. Generates JpaRepository interfaces for types annotated with `@glRepository`. | ## Step 4 — Run the generator If your config is named `glink.json`, `glink.yaml`, or `glink.yml`, just run: ```bash title="Terminal" glink ``` `glink` searches for those filenames from the current directory upward. To use a custom path, pass it explicitly: ```bash title="Terminal" glink -c my-config.json ``` For the Dart client config, the generator produces 21 files. For the Java client, 38 files. For Spring Boot, 9 files. Here is the Dart output tree: ``` lib/generated/ client/ graph_link_client.dart enums/ fuel_type.dart inputs/ add_person_input.dart add_vehicle_input.dart types/ vehicle.dart person.dart get_vehicle_response.dart list_vehicles_response.dart get_person_response.dart add_vehicle_response.dart add_person_response.dart vehicle_added_response.dart graph_link_error.dart graph_link_payload.dart ... + 6 internal support files ``` And the Spring Boot output: ``` src/main/java/com/example/generated/ controllers/ PersonServiceController.java ← generated, never touch VehicleServiceController.java ← generated, never touch services/ PersonService.java ← implement this VehicleService.java ← implement this types/ Person.java Vehicle.java inputs/ AddPersonInput.java AddVehicleInput.java enums/ FuelType.java ``` !!! info The `services/` files are the ones you implement. Controllers are generated and never touched by hand. Types, inputs, and enums are plain data classes. ## Step 5 — What just happened? The generator processed each section of your schema and produced a corresponding set of files: ### Types → model classes `type Vehicle` became `vehicle.dart` (Dart) and `Vehicle.java` (Java). Each has all fields, a constructor, and JSON serialization. In Dart, fields are final and the constructor uses named parameters with `required`. In Java, an inner `Builder` class is generated. ### Enums → enum classes with serialization `enum FuelType` became `fuel_type.dart` and `FuelType.java`, each with `toJson()` and `fromJson()` methods that map to and from the GraphQL string representation. ### Inputs → immutable input classes `input AddVehicleInput` became `add_vehicle_input.dart` and `AddVehicleInput.java`. Required fields are enforced at construction time. The Java version uses a builder with `Objects.requireNonNull` for required fields. ### Queries/Mutations/Subscriptions → response types + client Each operation generates a response wrapper (e.g. `GetVehicleResponse`) that holds the typed result. The `GraphLinkClient` class exposes `client.queries`, `client.mutations`, and `client.subscriptions` with one method per operation. ### Cache directives → wired into the client automatically The `@glCache` and `@glCacheInvalidate` directives you wrote in the schema are reflected in the generated client code. No application-level code is needed to enable caching. ## Watch mode During development, add `-w` to watch your schema files and regenerate automatically on every save: ```bash title="Terminal" glink -w # Output: # Watching schema/*.graphql for changes... # [14:32:01] Change detected in schema/schema.graphql # [14:32:01] Regenerating... done (21 files, 312ms) ``` Regeneration waits until changes have settled for 500 ms, so a burst of saves triggers a single run. Tune it with `--debounce `: ```bash title="Terminal" glink -w --debounce 1000 ``` This integrates naturally with Flutter's hot reload workflow — schema change, save, and your Dart types are updated before you switch back to the emulator. ## Next steps You now have generated code. The next step depends on your target: - [Dart / Flutter client](dart-client.md) — set up the adapter, initialize the client, make queries - [Java client](java-client.md) — wire up Jackson or Gson, make type-safe calls - [Spring Boot server](spring-server.md) — implement the generated service interfaces, run your app - [Caching](caching.md) — understand TTL, tags, and partial query caching in depth --- # Dart / Flutter Client A fully typed GraphQL client generated directly from your schema. ## Generated adapters GraphLink generates adapter files alongside the client. Which HTTP adapter is generated is controlled by the `httpAdapter` option in `glink.json`. The WebSocket adapter is always generated when your schema has subscriptions. === "JSON" ```json title="glink.json — adapter options" { "clientConfig": { "dart": { "packageName": "com.example.generated", "httpAdapter": "http" } } } ``` === "YAML" ```yaml title="glink.yaml — adapter options" clientConfig: dart: packageName: com.example.generated httpAdapter: http ``` **`httpAdapter`** — controls which HTTP adapter file is generated: - `"http"` (default) — generates `graph_link_http_adapter.dart` containing `GraphLinkHttpAdapter`. Uses `package:http` with an optional async `headersProvider` for custom headers. - `"dio"` — generates `graph_link_dio_adapter.dart` containing `GraphLinkDioAdapter`. Supports an optional `tokenProvider` for Bearer auth, custom Dio `interceptors`, and `BaseOptions`. Used automatically by `GraphLinkClient.withHttp`. - `"none"` — no HTTP adapter file is generated. Supply your own `Future Function(String)`. === "Dio adapter" ```dart // generated/client/graph_link_dio_adapter.dart class GraphLinkDioAdapter { final String url; final Dio dio; GraphLinkDioAdapter({ required this.url, Dio? dio, Future Function()? tokenProvider, // adds Authorization: Bearer List interceptors = const [], BaseOptions? options, }); Future call(String payload) async { /* ... */ } } ``` === "http adapter" ```dart // generated/client/graph_link_http_adapter.dart class GraphLinkHttpAdapter { final String url; final Future?>? Function()? headersProvider; GraphLinkHttpAdapter({ required this.url, this.headersProvider, // merged into request headers on every call }); Future call(String payload) async { /* ... */ } } ``` === "WebSocket adapter" ```dart // generated/client/graph_link_websocket_adapter.dart // Always generated when your schema has subscriptions. class DefaultGraphLinkWebSocketAdapter extends GraphLinkWebSocketAdapter { DefaultGraphLinkWebSocketAdapter({ required String url, Future?>? Function()? headersProvider, Duration initialReconnectDelay = const Duration(seconds: 1), Duration maxReconnectDelay = const Duration(seconds: 30), }); // Handles graphql-ws protocol, exponential-backoff reconnect, // and forwards headersProvider result as connection_init payload. } ``` All adapter files are generated into your output directory. They carry no external GraphLink dependency and you can edit them freely. ## Initializing the client The `GraphLinkClient` uses **named parameters**. Three constructors cover every scenario: === "withHttp (simplest)" ```dart import 'generated/client/graph_link_client.dart'; // Uses the generated Dio adapter for HTTP and the generated // WebSocket adapter for subscriptions. final client = GraphLinkClient.withHttp( url: 'http://localhost:8080/graphql', wsUrl: 'ws://localhost:8080/graphql', // Optional — adds Authorization: Bearer on every request tokenProvider: () async => await getAuthToken(), // Optional — forwarded as connection_init payload for WS auth wsTokenProvider: () async => await getAuthToken(), // Optional — defaults to InMemoryGraphLinkCacheStore // store: MyPersistentCacheStore(), ); ``` === "fromUrl (custom HTTP)" ```dart import 'generated/client/graph_link_client.dart'; import 'generated/client/graph_link_http_adapter.dart'; // Bring your own HTTP adapter (or use GraphLinkHttpAdapter). // The generated WebSocket adapter handles subscriptions. final client = GraphLinkClient.fromUrl( adapter: GraphLinkHttpAdapter( url: 'http://localhost:8080/graphql', headersProvider: () async => {'Authorization': 'Bearer ${await getToken()}'}, ).call, wsUrl: 'ws://localhost:8080/graphql', ); ``` === "full control" ```dart import 'generated/client/graph_link_client.dart'; import 'generated/client/graph_link_websocket_adapter.dart'; // Full control — any adapter function, any WebSocket adapter. final client = GraphLinkClient( adapter: myHttpAdapterFn, // Future Function(String payload) wsAdapter: DefaultGraphLinkWebSocketAdapter( url: 'ws://localhost:8080/graphql', headersProvider: () async => {'Authorization': 'Bearer ${await getToken()}'}, initialReconnectDelay: const Duration(seconds: 2), maxReconnectDelay: const Duration(seconds: 60), ), store: myPersistentCacheStore, // optional ); ``` !!! info "Testing made easy" Because the adapter is just a named function parameter, passing a mock in tests requires no HTTP mocking library: `adapter: (payload) async => '{"data":{"getCar":{"id":"1","make":"Toyota"}}}'` ## Queries All queries are accessible via `client.queries`. Each operation becomes a method with typed parameters and a typed return value. ```dart title="Fetching a vehicle by ID" // getVehicle returns GetVehicleResponse — never null (Vehicle! in schema) final res = await client.queries.getVehicle(id: '42'); // res.getVehicle is a typed Vehicle object print(res.getVehicle.brand); // Toyota print(res.getVehicle.model); // Camry print(res.getVehicle.year); // 2023 print(res.getVehicle.fuelType); // FuelType.GASOLINE ``` The generated `GetVehicleResponse` type looks like this: ```dart title="generated/types/get_vehicle_response.dart" class GetVehicleResponse { final Vehicle getVehicle; GetVehicleResponse({required this.getVehicle}); static GetVehicleResponse fromJson(Map json) { return GetVehicleResponse( getVehicle: Vehicle.fromJson(json['getVehicle'] as Map), ); } } ``` !!! info "toJson/fromJson generation is mandatory (v5.0.0+)" Every generated type, input, enum, and interface always gets `toJson`/`fromJson` — there is no config flag to suppress it. This matters because [identifier normalization](configuration.md#identifier-normalization) can make a field's generated Dart name diverge from its wire name (e.g. a keyword-colliding or non-canonically-cased GraphQL field): code that reads or writes generated objects by field access is always in the normalized name, and only `toJson`/`fromJson` know how to bridge back to the wire format. ### List queries ```dart title="Fetching all vehicles" final res = await client.queries.listVehicles(); // res.listVehicles is List — fully typed for (final vehicle in res.listVehicles) { print('${vehicle.brand} ${vehicle.model} (${vehicle.year})'); } ``` ## Nullable queries When the schema declares a query with a nullable return type (no `!`), the response wrapper field is also nullable: ```dart title="getPerson — nullable result" // Schema: getPerson(id: ID!): Person <-- no ! on Person final res = await client.queries.getPerson(id: '99'); // res.getPerson is Person? — use null-aware access if (res.getPerson != null) { print(res.getPerson!.name); } // Or with null-safe chaining print(res.getPerson?.email ?? 'Not found'); ``` ## Mutations Mutations live under `client.mutations` and follow the same pattern. Input types are passed as named parameters: ```dart title="Adding a vehicle" import 'generated/inputs/add_vehicle_input.dart'; import 'generated/enums/fuel_type.dart'; final added = await client.mutations.addVehicle( input: AddVehicleInput( brand: 'Toyota', model: 'Camry', year: 2023, fuelType: FuelType.GASOLINE, // ownerId is nullable — omit it or pass null ), ); print(added.addVehicle.id); // server-assigned ID print(added.addVehicle.brand); // Toyota ``` The generated `AddVehicleInput` class enforces required fields at construction time through Dart's named required parameters: ```dart title="generated/inputs/add_vehicle_input.dart" class AddVehicleInput { final String brand; final String model; final int year; final FuelType fuelType; final String? ownerId; AddVehicleInput({ required this.brand, required this.model, required this.year, required this.fuelType, this.ownerId }); Map toJson() { return { 'brand': brand, 'model': model, 'year': year, 'fuelType': fuelType.toJson(), 'ownerId': ownerId }; } } ``` ## Subscriptions Subscriptions are available via `client.subscriptions` and return a `Stream`. The generated `DefaultGraphLinkWebSocketAdapter` implements the [graphql-ws subprotocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) and handles connection init, ping/pong, and reconnect automatically. ```dart title="Subscribing to new vehicles" final subscription = client.subscriptions.vehicleAdded().listen((event) { print('New vehicle: ${event.vehicleAdded.brand} ${event.vehicleAdded.model}'); }); // Cancel when done (e.g. in Flutter widget dispose) await subscription.cancel(); ``` If you supply a `headersProvider` on the WebSocket adapter, its result is returned from `connectionInitPayload()` and sent in the `connection_init` message — your server can use it to authenticate the WebSocket session without needing HTTP headers on the upgrade request. ## Error handling If the server returns a GraphQL error, the generated client throws a `GraphLinkException` containing the list of errors from the response. Wrap calls in a try/catch: ```dart title="Error handling" import 'generated/types/graph_link_error.dart'; try { final res = await client.queries.getVehicle(id: 'bad-id'); print(res.getVehicle.brand); } on GraphLinkException catch (e) { for (final error in e.errors) { print('GraphQL error: ${error.message}'); if (error.locations != null) { for (final loc in error.locations!) { print(' at line ${loc.line}, column ${loc.column}'); } } } } catch (e) { // Network error, timeout, etc. print('Request failed: $e'); } ``` ## The _all_fields fragment When `generateAllFieldsFragments: true` is set in the config, GraphLink generates a named fragment for every type in the schema. The `autoGenerateQueries: true` option uses these fragments to automatically build the query strings for every operation. Instead of writing query strings by hand, GraphLink inlines the fields from the fragment. This means you never have to maintain query strings manually — when you add a field to a type in the schema, the generated client automatically fetches that field. You can also reference `_all_fields_Vehicle` by name in any hand-written queries. Use the shorthand `... _all_fields` and GraphLink resolves it to the appropriate type-specific fragment based on the field's return type. ```graphql title="Using _all_fields in a custom query" type Query { # GraphLink resolves _all_fields to _all_fields_Vehicle for this field getVehicle(id: ID!): Vehicle! @glCache(ttl: "2m", tags: ["vehicles"]) } ``` ## Barrel file GraphLink emits a single barrel file, `graphlink.dart`, alongside the generated output. It re-exports every generated enum, input, type, and interface — plus the client files, so `withClientFiles` output is covered too — from one place: ```dart title="generated/graphlink.dart" export 'enums/fuel_type.dart'; export 'inputs/add_vehicle_input.dart'; export 'types/vehicle.dart'; export 'types/get_vehicle_response.dart'; export 'client/graph_link_client.dart'; // ... one export line per generated file ``` Import it once instead of reaching into individual files under `enums/`, `inputs/`, `types/`, and `client/`: ```dart title="Using the barrel file" import 'package:my_app/generated/graphlink.dart'; final client = GraphLinkClient.withHttp(url: 'http://localhost:8080/graphql'); final res = await client.queries.getVehicle(id: '42'); // Vehicle, AddVehicleInput, etc. all in scope ``` The barrel file is regenerated on every run alongside everything else — there's nothing to configure. ## Operation name in URL Set `"operationNameAsParameter": true` to include the operation name as a URL query parameter on every request. This is useful for identifying operations in server logs, APM dashboards, and proxies without parsing the request body: === "JSON" ```json title="glink.json" { "clientConfig": { "dart": { "operationNameAsParameter": true } } } ``` === "YAML" ```yaml title="glink.yaml" clientConfig: dart: operationNameAsParameter: true ``` The generated adapter function gains a second parameter and the client appends `?operationName=OperationName` to every HTTP request: ```dart // Generated adapter signature with operationNameAsParameter: true Future Function(String payload, String operationName) adapter ``` The WebSocket adapter is unaffected — operation names on subscriptions flow through the graphql-ws protocol message payload. ## Nullable fields By default, nullable schema fields generate optional constructor parameters (`this.fieldName`). Set `"nullableFieldsRequired": true` to force `required this.fieldName` on every field regardless of nullability: === "JSON" ```json title="glink.json" { "clientConfig": { "dart": { "nullableFieldsRequired": true } } } ``` === "YAML" ```yaml title="glink.yaml" clientConfig: dart: nullableFieldsRequired: true ``` With the default `false`, a nullable field like `ownerId: ID` generates `this.ownerId` (optional). With `true`, it generates `required this.ownerId` — you must always pass it explicitly, even as `null`. ## File uploads When your schema uses the built-in `Upload` scalar, GraphLink generates a `GLUpload` type and produces multipart-aware upload logic in the HTTP adapter: ```graphql title="Schema — Upload scalar" scalar Upload type Mutation { uploadDocument(file: Upload!): String! } ``` ```dart title="Using GLUpload in Dart" import 'generated/types/gl_upload.dart'; import 'dart:io'; final file = File('/path/to/document.pdf'); final upload = GLUpload( stream: file.openRead(), length: await file.length(), filename: 'document.pdf', mimeType: 'application/pdf', ); final result = await client.mutations.uploadDocument(file: upload); print(result.uploadDocument); // e.g. "https://cdn.example.com/document.pdf" ``` The Dio and http adapters both handle multipart encoding automatically when a mutation argument contains a `GLUpload`. No extra configuration is needed. --- # Java Client Type-safe. No generics. No casting. Works with any JSON library. ## Generated adapters GraphLink generates concrete adapter classes into your `client/` folder — no external GraphLink runtime, no boilerplate. Two config options control what is generated: === "JSON" ```json title="glink.json — adapter options" { "clientConfig": { "java": { "packageName": "com.example.generated", "wsAdapter": "java11", "jsonCodec": "jackson" } } } ``` === "YAML" ```yaml title="glink.yaml — adapter options" clientConfig: java: packageName: com.example.generated wsAdapter: java11 jsonCodec: jackson ``` **`wsAdapter`** — controls which WebSocket adapter is generated: - `"java11"` (default) — generates `DefaultGraphLinkWebSocketAdapter` using Java 11's built-in `java.net.http.WebSocket`. Zero external dependencies. Supports exponential-backoff auto-reconnect, an optional `Supplier>` for auth headers, and automatically forwards those headers as the `connection_init` payload so your server can authenticate the WebSocket handshake. - `"okhttp"` — generates the same interface implemented with OkHttp's WebSocket client instead. - `"none"` — no WebSocket adapter is generated. Use this if you do not need subscriptions. **`jsonCodec`** — controls which JSON codec is generated: - `"jackson"` (default) — generates `JacksonGraphLinkJsonCodec` implementing both `GraphLinkJsonEncoder` and `GraphLinkJsonDecoder`. - `"gson"` — generates `GsonGraphLinkJsonCodec` instead. - `"none"` — no codec class is generated; supply your own lambdas. `DefaultGraphLinkClientAdapter` is always generated (when `wsAdapter` is not `"none"`). It uses Java 11's `HttpClient` for HTTP requests and accepts the same optional headers provider as the WebSocket adapter. ### Injectable HTTP client Both `DefaultGraphLinkClientAdapter` and `DefaultGraphLinkWebSocketAdapter` accept a pre-configured `HttpClient` (Java 11) or `OkHttpClient` as an optional constructor argument: === "Java 11 HttpClient" ```java HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .sslContext(mySslContext) .proxy(ProxySelector.of(new InetSocketAddress("proxy.corp", 8080))) .build(); // Pass it to the adapter — all HTTP and WebSocket traffic uses this client DefaultGraphLinkClientAdapter adapter = new DefaultGraphLinkClientAdapter( "http://api.example.com/graphql", httpClient ); DefaultGraphLinkWebSocketAdapter wsAdapter = new DefaultGraphLinkWebSocketAdapter( "ws://api.example.com/graphql", httpClient ); ``` === "OkHttpClient" ```java OkHttpClient okHttpClient = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .addInterceptor(loggingInterceptor) .build(); DefaultGraphLinkClientAdapter adapter = new DefaultGraphLinkClientAdapter( "http://api.example.com/graphql", okHttpClient ); DefaultGraphLinkWebSocketAdapter wsAdapter = new DefaultGraphLinkWebSocketAdapter( "ws://api.example.com/graphql", okHttpClient ); ``` The no-arg and headers-only constructors still work unchanged — the injected client is an opt-in. ## Initializing the client The generated `GraphLinkClient` ships with several constructors that progressively add control: === "one-liner" ```java import com.example.generated.client.GraphLinkClient; // Simplest setup — Jackson + Java 11 HttpClient + auto-derives wsUrl. // wsUrl is derived by replacing "http" with "ws" in the provided URL. GraphLinkClient client = new GraphLinkClient("http://localhost:8080/graphql"); ``` === "with auth headers" ```java // Dynamic auth headers on every request — token is fetched fresh each time. Supplier> headers = () -> Map.of( "Authorization", "Bearer " + tokenService.getToken() ); // wsUrl auto-derived; both HTTP and WebSocket adapters receive the headers. GraphLinkClient client = new GraphLinkClient( "http://localhost:8080/graphql", headers, new JacksonGraphLinkJsonCodec(), new JacksonGraphLinkJsonCodec() ); ``` === "custom codec" ```java // Use a different JSON library — e.g. Gson. GraphLinkJsonEncoder encoder = obj -> gson.toJson(obj); GraphLinkJsonDecoder decoder = json -> gson.fromJson(json, Map.class); GraphLinkClient client = new GraphLinkClient( "http://localhost:8080/graphql", "ws://localhost:8080/graphql", encoder, decoder ); ``` === "full control" ```java // Full manual wiring — bring your own adapter, encoder, decoder, and cache store. GraphLinkClientAdapter adapter = payload -> { /* custom HTTP logic */ }; GraphLinkJsonEncoder encoder = obj -> mapper.writeValueAsString(obj); GraphLinkJsonDecoder decoder = json -> mapper.readValue(json, Map.class); GraphLinkWebSocketAdapter wsAdapter = new DefaultGraphLinkWebSocketAdapter( "ws://localhost:8080/graphql" ); GraphLinkClient client = new GraphLinkClient( adapter, encoder, decoder, myCacheStore, wsAdapter ); ``` !!! info "Any JSON library works" The three generated interfaces (`GraphLinkClientAdapter`, `GraphLinkJsonEncoder`, `GraphLinkJsonDecoder`) are all `@FunctionalInterface` — assign them from lambdas using Gson, Moshi, or any other library. Jackson is only the default for the convenience constructors. Pass a custom `GraphLinkCacheStore` in the full constructor if you need a persistent or shared cache (e.g. Redis-backed). See the [Caching](caching.md) page for details. ## Queries — no generics This is the core difference from every other Java GraphQL client. There are no `TypeReference` anonymous classes, no unchecked casts, no raw `Map` navigation: ```java title="Fetching a vehicle — GraphLink style" // Clean, typed, no generics GetVehicleResponse res = client.queries.getVehicle("42"); System.out.println(res.getGetVehicle().getBrand()); // Toyota System.out.println(res.getGetVehicle().getYear()); // 2023 System.out.println(res.getGetVehicle().getFuelType()); // GASOLINE ``` Compare this to the boilerplate required by most other clients: ```java title="The same query — typical other client" // What you're forced to write with most Java GraphQL clients GraphQLResponse> response = client.query(new SimpleGraphQLRequest<>( "query getVehicle($id: ID!) { getVehicle(id: $id) { id brand model year fuelType } }", Map.of("id", "42"), new TypeReference>>() {} )); @SuppressWarnings("unchecked") Map vehicleMap = (Map) response.getData().get("getVehicle"); String brand = (String) vehicleMap.get("brand"); Integer year = ((Number) vehicleMap.get("year")).intValue(); ``` ## Nullable queries When the schema declares a nullable return type (no `!`), the corresponding getter on the response class returns a nullable type: ```java title="getPerson — nullable result" // Schema: getPerson(id: ID!): Person <-- nullable return GetPersonResponse res = client.queries.getPerson("99"); Person p = res.getGetPerson(); // can be null — check before use if (p != null) { System.out.println(p.getName()); System.out.println(p.getEmail()); } else { System.out.println("Person not found"); } ``` ## Mutations — builder pattern All input types are generated with an inner `Builder` class. Required fields (non-nullable in the schema) are validated with `Objects.requireNonNull` when `build()` is called: ```java title="Adding a vehicle" import com.example.generated.inputs.AddVehicleInput; import com.example.generated.enums.FuelType; AddVehicleResponse added = client.mutations.addVehicle( AddVehicleInput.builder() .brand("Toyota") .model("Camry") .year(2023) .fuelType(FuelType.GASOLINE) // ownerId is nullable — omit for null .build() ); System.out.println(added.getAddVehicle().getId()); // server-assigned ID System.out.println(added.getAddVehicle().getBrand()); // Toyota ``` The generated `AddVehicleInput` class: ```java title="generated/inputs/AddVehicleInput.java" public class AddVehicleInput { private final String brand; private final String model; private final Integer year; private final FuelType fuelType; private final String ownerId; public AddVehicleInput(String brand, String model, Integer year, FuelType fuelType, String ownerId) { Objects.requireNonNull(brand); Objects.requireNonNull(model); Objects.requireNonNull(year); Objects.requireNonNull(fuelType); this.brand = brand; this.model = model; this.year = year; this.fuelType = fuelType; this.ownerId = ownerId; } public static Builder builder() { return new Builder(); } public static class Builder { private String brand; private String model; private Integer year; private FuelType fuelType; private String ownerId; public Builder brand(String brand) { this.brand = brand; return this; } public Builder model(String model) { this.model = model; return this; } public Builder year(Integer year) { this.year = year; return this; } public Builder fuelType(FuelType fuelType) { this.fuelType = fuelType; return this; } public Builder ownerId(String ownerId) { this.ownerId = ownerId; return this; } public AddVehicleInput build() { return new AddVehicleInput(brand, model, year, fuelType, ownerId); } } } ``` ## Lists List queries return a typed `List` — no casting required: ```java title="List query" ListVehiclesResponse res = client.queries.listVehicles(); List vehicles = res.getListVehicles(); // List — no raw types for (Vehicle v : vehicles) { System.out.printf("%s %s (%d) — %s%n", v.getBrand(), v.getModel(), v.getYear(), v.getFuelType()); } // Or with streams vehicles.stream() .filter(v -> v.getFuelType() == FuelType.ELECTRIC) .map(Vehicle::getBrand) .forEach(System.out::println); ``` ## The response wrapper pattern Every query, mutation, and subscription operation generates a dedicated response class named `{OperationName}Response`. For example, `getVehicle` generates `GetVehicleResponse`. This pattern mirrors the GraphQL JSON response structure, which always wraps results in a `data` field: ```json title="GraphQL HTTP response JSON" { "data": { "getVehicle": { "id": "42", "brand": "Toyota", "model": "Camry", "year": 2023, "fuelType": "GASOLINE", "ownerId": null } } } ``` The generated `GetVehicleResponse.fromJson()` navigates into the `data` object and deserializes `getVehicle` as a `Vehicle`. From your code's perspective, you simply call `res.getGetVehicle()` — the JSON unwrapping is invisible. Notice the double "get" in `getGetVehicle()` — the first is the Java getter prefix, the second is the operation name. This is consistent and predictable: the method name is always `get` + the operation name with a capital first letter. !!! info "toJson/fromJson generation is mandatory (v5.0.0+)" Every generated type, input, enum, and interface always gets `toJson`/`fromJson` — there is no config flag to suppress it. This matters because [identifier normalization](configuration.md#identifier-normalization) can make a field's generated Java name diverge from its wire name (e.g. a keyword-colliding or non-canonically-cased GraphQL field): code that calls generated getters is always working with the normalized name, and only `toJson`/`fromJson` know how to bridge back to the wire format. ## Subscriptions Subscriptions are available via `client.subscriptions` and use the `GraphLinkWebSocketAdapter` interface. The generated `DefaultGraphLinkWebSocketAdapter` implements this interface out of the box: ```java title="Subscribing to new vehicles" client.subscriptions.vehicleAdded(event -> { VehicleAddedResponse res = event; System.out.println("New vehicle: " + res.getVehicleAdded().getBrand()); }); ``` The generated WebSocket adapter handles the [graphql-ws subprotocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) automatically — connection init, ping/pong, and exponential-backoff reconnect on disconnect. !!! info "Deriving the WebSocket URL" The convenience constructors that take only an HTTP URL automatically derive the WebSocket URL by replacing `http` with `ws` (and `https` with `wss`). Pass an explicit `wsUrl` if your WebSocket endpoint differs from the HTTP endpoint. ## Operation name in URL Set `"operationNameAsParameter": true` to append the operation name as a URL query parameter on every request — useful for server-side logging and APM tracing: === "JSON" ```json title="glink.json" { "clientConfig": { "java": { "operationNameAsParameter": true } } } ``` === "YAML" ```yaml title="glink.yaml" clientConfig: java: operationNameAsParameter: true ``` The generated adapter interface gains an `operationName` parameter and the client appends `?operationName=OperationName` to each HTTP request URL. ## Nullable fields By default, nullable schema fields accept `null` without any runtime enforcement. Set `"nullableFieldsRequired": true` to emit `Objects.requireNonNull` on every field, including nullable ones: === "JSON" ```json title="glink.json" { "clientConfig": { "java": { "nullableFieldsRequired": true } } } ``` === "YAML" ```yaml title="glink.yaml" clientConfig: java: nullableFieldsRequired: true ``` ## Java records Set `"inputAsRecord": true` and/or `"typeAsRecord": true` to generate inputs and types as Java records instead of classes with builders. Records use component accessor syntax (`field()`) instead of `getField()`: === "JSON" ```json title="glink.json" { "clientConfig": { "java": { "inputAsRecord": true, "typeAsRecord": true } } } ``` === "YAML" ```yaml title="glink.yaml" clientConfig: java: inputAsRecord: true typeAsRecord: true ``` ```java title="Generated input as record" public record AddVehicleInput( String brand, String model, Integer year, FuelType fuelType, String ownerId ) {} // Usage — no builder needed AddVehicleInput input = new AddVehicleInput("Toyota", "Camry", 2023, FuelType.GASOLINE, null); System.out.println(input.brand()); // component accessor, no get prefix ``` Records require Java 16 or later. Mapping methods generated by `@glMapsTo` use component accessors automatically when `typeAsRecord` is `true`. ## Reactive client (v5.0.0+) By default the Java client is blocking — every query/mutation method returns its result directly and subscriptions notify via a listener callback (see above). Set `"asyncStyle"` in `clientConfig.java` to generate a reactive client instead: === "JSON" ```json title="glink.json — reactive client" { "clientConfig": { "java": { "asyncStyle": "reactor", "reactiveHttpClient": "jdk" } } } ``` === "YAML" ```yaml title="glink.yaml — reactive client" clientConfig: java: asyncStyle: reactor reactiveHttpClient: jdk ``` | `asyncStyle` | Query/mutation return type | Subscription return type | Library | |---|---|---|---| | `"blocking"` (default) | `T` | listener callback | — | | `"reactor"` | `Mono` | `Flux` | Project Reactor | | `"rxjava3"` | `Single` | `Observable` | RxJava 3 | | `"mutiny"` | `Uni` | `Multi` | SmallRye Mutiny | ```java title="Reactive query — asyncStyle: reactor" Mono res = client.queries.getVehicle("42"); res.subscribe(r -> System.out.println(r.getGetVehicle().getBrand())); ``` `reactiveHttpClient` selects the default adapter's transport: - `"jdk"` (default) — uses `HttpClient.sendAsync`. Works with every `asyncStyle`, no extra dependency beyond the reactive library itself. - `"webclient"` — uses Spring `WebClient`. Only compatible with `asyncStyle: "reactor"`. File upload support is available for the Java client regardless of `asyncStyle` — see below. ## File uploads When your schema uses the built-in `Upload` scalar, GraphLink generates a `GLUpload` class and handles multipart encoding in the generated adapter: ```java title="Using GLUpload in Java" import com.example.generated.types.GLUpload; import java.io.FileInputStream; import java.io.File; File file = new File("/path/to/document.pdf"); GLUpload upload = new GLUpload( new FileInputStream(file), file.length(), "document.pdf", "application/pdf" ); UploadDocumentResponse result = client.mutations.uploadDocument(upload); System.out.println(result.getUploadDocument()); // URL or ID returned by server ``` The generated `DefaultGraphLinkClientAdapter` automatically encodes mutations containing `GLUpload` as multipart/form-data following the [GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec). No extra configuration needed. --- # TypeScript Client A fully typed GraphQL client generated from your schema — works in Angular, React, Vue, Svelte, and Node/Bun. ## Configuration Set `"mode": "client"` and add a `"typescript"` key under `clientConfig`: === "JSON" ```json title="glink.json" { "schemaPaths": ["schema/*.graphql"], "mode": "client", "typeMappings": { "ID": "string", "String": "string", "Float": "number", "Int": "number", "Boolean": "boolean", "Null": "null" }, "outputDir": "src/generated", "clientConfig": { "typescript": { "packageName": "my-app", "generateAllFieldsFragments": true, "autoGenerateQueries": true, "httpAdapter": "fetch", "observables": false, "generateDefaultWsAdapter": true, "optionalNullableInputFields": true } } } ``` === "YAML" ```yaml title="glink.yaml" schemaPaths: - schema/*.graphql mode: client typeMappings: ID: string String: string Float: number Int: number Boolean: boolean Null: null outputDir: src/generated clientConfig: typescript: packageName: my-app generateAllFieldsFragments: true autoGenerateQueries: true httpAdapter: fetch observables: false generateDefaultWsAdapter: true optionalNullableInputFields: true ``` ## Generated output For the example schema, GraphLink generates the following TypeScript files: ``` src/generated/ client/ graph-link-client.ts ← GraphLinkClient class graph-link-fetch-adapter.ts ← HTTP adapter graph-link-ws-adapter.ts ← WebSocket adapter types/ vehicle.ts person.ts inputs/ add-vehicle-input.ts add-person-input.ts enums/ fuel-type.ts responses/ get-vehicle-response.ts list-vehicles-response.ts add-vehicle-response.ts graphlink.ts ← barrel file — re-exports everything above ``` ## Barrel file Alongside the per-file output, GraphLink emits a single barrel file, `graphlink.ts`, that re-exports every generated enum, input, type, and interface, plus the client files: ```typescript title="generated/graphlink.ts" export * from './enums/fuel-type'; export * from './inputs/add-vehicle-input'; export * from './types/vehicle'; export * from './responses/get-vehicle-response'; export * from './client/graph-link-client'; // ... one export line per generated file ``` Import from it instead of reaching into individual subdirectories: ```typescript title="Using the barrel file" import { GraphLinkClient, Vehicle, AddVehicleInput } from './generated/graphlink'; ``` It's regenerated on every run — nothing to configure. ## HTTP adapters The `httpAdapter` option controls which HTTP adapter is generated: - `"fetch"` (default) — generates `GraphLinkFetchAdapter` using the native `fetch` API. Works in Angular, React, Vue, Svelte, Bun, and modern Node (v18+) without any extra dependencies. - `"axios"` — generates `GraphLinkAxiosAdapter` using Axios. Choose this for Node environments where you already use Axios or need its interceptor/cancellation support. === "Fetch adapter" ```typescript // generated/client/graph-link-fetch-adapter.ts export class GraphLinkFetchAdapter { constructor( private readonly url: string, private readonly headersProvider?: () => Promise | undefined> ) {} async call(payload: string): Promise { const headers: Record = { 'Content-Type': 'application/json', ...((await this.headersProvider?.()) ?? {}), }; const res = await fetch(this.url, { method: 'POST', headers, body: payload }); return res.text(); } } ``` === "Axios adapter" ```typescript // generated/client/graph-link-axios-adapter.ts import axios, { AxiosInstance } from 'axios'; export class GraphLinkAxiosAdapter { private readonly axios: AxiosInstance; constructor( private readonly url: string, private readonly headersProvider?: () => Promise | undefined>, axiosInstance?: AxiosInstance ) { this.axios = axiosInstance ?? axios.create(); } async call(payload: string): Promise { const headers = { 'Content-Type': 'application/json', ...((await this.headersProvider?.()) ?? {}), }; const res = await this.axios.post(this.url, payload, { headers }); return typeof res.data === 'string' ? res.data : JSON.stringify(res.data); } } ``` === "WebSocket adapter" ```typescript // generated/client/graph-link-ws-adapter.ts // Generated when generateDefaultWsAdapter: true export class DefaultGraphLinkWebSocketAdapter { constructor( private readonly url: string, private readonly headersProvider?: () => Promise | undefined>, private readonly initialReconnectDelayMs: number = 1000, private readonly maxReconnectDelayMs: number = 30000 ) {} // Implements graphql-ws protocol with exponential-backoff reconnect. // headersProvider result is forwarded as connection_init payload. } ``` ## Initializing the client === "Minimal (Fetch)" ```typescript import { GraphLinkClient } from './generated/client/graph-link-client'; import { GraphLinkFetchAdapter } from './generated/client/graph-link-fetch-adapter'; import { DefaultGraphLinkWebSocketAdapter } from './generated/client/graph-link-ws-adapter'; const adapter = new GraphLinkFetchAdapter('http://localhost:8080/graphql'); const wsAdapter = new DefaultGraphLinkWebSocketAdapter('ws://localhost:8080/graphql'); const client = new GraphLinkClient(adapter.call.bind(adapter), wsAdapter); ``` === "With auth headers" ```typescript import { GraphLinkFetchAdapter } from './generated/client/graph-link-fetch-adapter'; const adapter = new GraphLinkFetchAdapter( 'http://localhost:8080/graphql', async () => ({ Authorization: `Bearer ${await getToken()}` }) ); const client = new GraphLinkClient(adapter.call.bind(adapter)); ``` === "Axios + subscriptions" ```typescript import { GraphLinkAxiosAdapter } from './generated/client/graph-link-axios-adapter'; import { DefaultGraphLinkWebSocketAdapter } from './generated/client/graph-link-ws-adapter'; const adapter = new GraphLinkAxiosAdapter( 'http://localhost:8080/graphql', async () => ({ Authorization: `Bearer ${await getToken()}` }) ); const wsAdapter = new DefaultGraphLinkWebSocketAdapter( 'ws://localhost:8080/graphql', async () => ({ Authorization: `Bearer ${await getToken()}` }) ); const client = new GraphLinkClient(adapter.call.bind(adapter), wsAdapter); ``` ## Queries All queries are accessible via `client.queries`. Each method is fully typed — parameters and return types match the schema exactly: ```typescript title="Fetching a vehicle" // getVehicle returns Promise const res = await client.queries.getVehicle({ id: '42' }); // res.getVehicle is typed as Vehicle console.log(res.getVehicle.brand); // Toyota console.log(res.getVehicle.year); // 2023 console.log(res.getVehicle.fuelType); // FuelType.GASOLINE ``` ```typescript title="generated/responses/get-vehicle-response.ts" import { Vehicle } from '../types/vehicle'; export interface GetVehicleResponse { getVehicle: Vehicle; } export function getVehicleResponseFromJson(json: Record): GetVehicleResponse { return { getVehicle: vehicleFromJson(json['getVehicle'] as Record), }; } ``` ### List queries ```typescript title="Fetching all vehicles" const res = await client.queries.listVehicles(); // res.listVehicles is Vehicle[] — fully typed res.listVehicles.forEach(v => { console.log(`${v.brand} ${v.model} (${v.year})`); }); // Filter with full type safety const electrics = res.listVehicles.filter(v => v.fuelType === FuelType.ELECTRIC); ``` ## Mutations Mutations are available via `client.mutations`. Input types use TypeScript interfaces with optional fields for nullable schema fields (when `optionalNullableInputFields: true`): ```typescript title="Adding a vehicle" import { FuelType } from './generated/enums/fuel-type'; const added = await client.mutations.addVehicle({ input: { brand: 'Toyota', model: 'Camry', year: 2023, fuelType: FuelType.GASOLINE, // ownerId is nullable — omit entirely or pass null }, }); console.log(added.addVehicle.id); // server-assigned ID console.log(added.addVehicle.brand); // Toyota ``` ```typescript title="generated/inputs/add-vehicle-input.ts" import { FuelType } from '../enums/fuel-type'; export interface AddVehicleInput { brand: string; model: string; year: number; fuelType: FuelType; ownerId?: string | null; // optional because nullable in schema } export function addVehicleInputToJson(input: AddVehicleInput): Record { return { brand: input.brand, model: input.model, year: input.year, fuelType: input.fuelType, ownerId: input.ownerId ?? null, }; } ``` ## Subscriptions Subscriptions are available via `client.subscriptions`. Subscription methods return an object with an `unsubscribe()` method: ```typescript title="Subscribing to vehicle events" const sub = client.subscriptions.vehicleAdded({ onData: (event) => { console.log('New vehicle:', event.vehicleAdded.brand); }, onError: (err) => console.error('Subscription error:', err), onComplete: () => console.log('Subscription ended'), }); // Later — clean up sub.unsubscribe(); ``` ## RxJS observables mode Set `"observables": true` in the TypeScript config to generate Observable-returning methods instead of Promise/callback. This is the idiomatic choice for Angular projects: ```typescript title="With observables: true" import { Observable } from 'rxjs'; // Queries and mutations return Observable client.queries.listVehicles().subscribe(res => { this.vehicles = res.listVehicles; }); // Subscriptions also return Observable client.subscriptions.vehicleAdded().subscribe(event => { this.vehicles.push(event.vehicleAdded); }); // In Angular templates — use the async pipe // vehicles$ = client.queries.listVehicles().pipe(map(r => r.listVehicles)); //
  • {{ v.brand }}
  • ``` ## Operation name in URL Set `"operationNameAsParameter": true` to append the operation name as a query parameter on every HTTP request — useful for server-side logging and APM: === "JSON" ```json title="glink.json" { "clientConfig": { "typescript": { "operationNameAsParameter": true } } } ``` === "YAML" ```yaml title="glink.yaml" clientConfig: typescript: operationNameAsParameter: true ``` The adapter function gains a second parameter and the client sends requests to `?operationName=OperationName`. ## Readonly types Set `"immutableTypeFields": true` (the default) to generate `readonly` on every field in type interfaces: ```typescript title="With immutableTypeFields: true (default)" export interface Vehicle { readonly id: string; readonly brand: string; readonly model: string; readonly year: number; } ``` Set to `false` if you need to mutate response objects after deserialization. ## Serialization and union discriminants (v5.0.0+) `toJson`/`fromJson` generation is mandatory since v5.0.0 for types, inputs, enums, and interfaces — there is no config flag to suppress it. The generated client decodes every response through the generated `fromJson` functions instead of casting the raw parsed JSON, so a field's wire name (from the schema) and its generated identifier (after [identifier normalization](configuration.md#identifier-normalization)) always stay in sync. TypeScript interfaces that implement a union or interface type also gain a `readonly __typename: 'TypeName'` discriminant field, plus a union-level `toJson()` that switches on `__typename`: ```typescript title="Generated union member and toJson dispatch" export interface Car extends SearchResult { readonly __typename: 'Car'; readonly id: string; readonly make: string; } export function searchResultToJson(value: SearchResult): Record { switch (value.__typename) { case 'Car': return carToJson(value as Car); case 'Owner': return ownerToJson(value as Owner); // ... } } ``` This completes proper discriminated-union (de)serialization on the TypeScript target — narrowing on `result.__typename` (or a `switch`) gives you the concrete member type with no manual casting. ## Error handling The generated client throws a `GraphLinkException` when the server returns GraphQL errors: ```typescript title="Error handling" import { GraphLinkException } from './generated/client/graph-link-client'; try { const res = await client.queries.getVehicle({ id: 'bad-id' }); console.log(res.getVehicle.brand); } catch (e) { if (e instanceof GraphLinkException) { for (const error of e.errors) { console.error('GraphQL error:', error.message); } } else { // Network error, timeout, etc. console.error('Request failed:', e); } } ``` !!! info "Testing without mocking frameworks" The adapter is a plain function — `(payload: string) => Promise`. In tests, pass a lambda that returns the JSON you want: `adapter: async () => JSON.stringify({ data: { getVehicle: { id: '1', brand: 'Toyota', ... } } })`. No HTTP mocking library needed. ## Using with Angular In Angular, inject the `GraphLinkClient` as a singleton service and use `observables: true` for the most ergonomic integration with Angular's reactive patterns: ```typescript title="Angular service setup" import { Injectable } from '@angular/core'; import { GraphLinkClient } from './generated/client/graph-link-client'; import { GraphLinkFetchAdapter } from './generated/client/graph-link-fetch-adapter'; import { DefaultGraphLinkWebSocketAdapter } from './generated/client/graph-link-ws-adapter'; @Injectable({ providedIn: 'root' }) export class ApiService { readonly client: GraphLinkClient; constructor(private authService: AuthService) { const adapter = new GraphLinkFetchAdapter( 'https://api.example.com/graphql', async () => ({ Authorization: `Bearer ${this.authService.getToken()}` }) ); const wsAdapter = new DefaultGraphLinkWebSocketAdapter( 'wss://api.example.com/graphql', async () => ({ Authorization: `Bearer ${this.authService.getToken()}` }) ); this.client = new GraphLinkClient(adapter.call.bind(adapter), wsAdapter); } } ``` ## Using with React Create the client once (outside the component tree) and use it directly in hooks: ```typescript title="React hook usage" // api.ts — create client once import { GraphLinkClient } from './generated/client/graph-link-client'; import { GraphLinkFetchAdapter } from './generated/client/graph-link-fetch-adapter'; const adapter = new GraphLinkFetchAdapter('/graphql'); export const api = new GraphLinkClient(adapter.call.bind(adapter)); // VehicleList.tsx import { useEffect, useState } from 'react'; import { api } from './api'; import { Vehicle } from './generated/types/vehicle'; export function VehicleList() { const [vehicles, setVehicles] = useState([]); useEffect(() => { api.queries.listVehicles().then(res => setVehicles(res.listVehicles)); }, []); return
      {vehicles.map(v =>
    • {v.brand} {v.model}
    • )}
    ; } ``` --- # Kotlin Client A fully typed GraphQL client generated from your schema — idiomatic Kotlin with coroutines, data classes, and `kotlinx.serialization`. ## Configuration Set `"mode": "client"` and add a `"kotlin"` key under `clientConfig`: === "JSON" ```json title="glink.json" { "schemaPaths": ["schema/*.graphql"], "mode": "client", "typeMappings": { "ID": "String", "String": "String", "Float": "Double", "Int": "Int", "Boolean": "Boolean" }, "outputDir": "src/main/kotlin/com/example/generated", "clientConfig": { "kotlin": { "packageName": "com.example.generated", "generateAllFieldsFragments": true, "autoGenerateQueries": true, "typeAsDataClass": true, "inputAsDataClass": true, "wsAdapter": "okhttp" } } } ``` === "YAML" ```yaml title="glink.yaml" schemaPaths: - schema/*.graphql mode: client typeMappings: ID: String String: String Float: Double Int: Int Boolean: Boolean outputDir: src/main/kotlin/com/example/generated clientConfig: kotlin: packageName: com.example.generated generateAllFieldsFragments: true autoGenerateQueries: true typeAsDataClass: true inputAsDataClass: true wsAdapter: okhttp ``` ## Generated output For a schema with `User`, `Post`, `CreateUserInput`, and a `UserStatus` enum, GraphLink generates: ``` src/main/kotlin/com/example/generated/ client/ GraphLinkClient.kt ← main client (queries / mutations / subscriptions) GraphLinkQueries.kt GraphLinkMutations.kt GraphLinkSubscriptions.kt GraphLinkWebSocketAdapter.kt ← WebSocket adapter interface GraphLinkMultipartAdapter.kt ← file-upload adapter interface KotlinxSerializationGraphLinkJsonCodec.kt ← default JSON codec GraphLinkCacheStore.kt ← cache store interface InMemoryGraphLinkCacheStore.kt ← in-memory cache implementation GraphLinkCacheEntry.kt GraphLinkTagEntry.kt GraphLinkPartialQuery.kt GraphLinkException.kt GLUpload.kt ← file upload type ... types/ User.kt Post.kt CreateUserResponse.kt ... inputs/ CreateUserInput.kt UpdateUserInput.kt enums/ UserStatus.kt interfaces/ GraphLinkClientAdapter.kt GraphLinkJsonEncoder.kt GraphLinkJsonDecoder.kt SearchResult.kt ← generated union/interface ``` ## Data classes Types and inputs are generated as `data class` by default (controlled by `typeAsDataClass` / `inputAsDataClass`). Each class includes `toJson()` and a companion `fromJson()`: ```kotlin // generated data class User( val id: String, val name: String, val email: String, val status: UserStatus, val address: Address, val billingAddress: Address? = null, val tags: List? = null, val scores: List? = null, ) data class CreateUserInput( val name: String, val email: String, val status: UserStatus, val address: AddressInput, val billingAddress: AddressInput? = null, val tags: List? = null, ) ``` Nullable fields have a `= null` default, so you only pass the fields you need. ## Enums ```kotlin // generated enum class UserStatus { ACTIVE, INACTIVE, SUSPENDED; companion object { fun fromJson(value: String): UserStatus = when (value) { "ACTIVE" -> ACTIVE "INACTIVE" -> INACTIVE "SUSPENDED" -> SUSPENDED else -> throw IllegalArgumentException("Unknown UserStatus: $value") } } fun toJson(): String = when (this) { ACTIVE -> "ACTIVE" INACTIVE -> "INACTIVE" SUSPENDED -> "SUSPENDED" } } ``` !!! danger "Breaking change in v5.0.0" `fromJson` used to be `fun fromJson(value: String?): T?`, returning `null` for an unrecognized wire value. As of v5.0.0 the signature is `fun fromJson(value: String): T` — an unrecognized value now throws `IllegalArgumentException` instead of silently returning `null`. `toJson`/`fromJson` also always use an explicit wire-name mapping now (rather than `.name`/`valueOf`), since [identifier normalization](configuration.md#identifier-normalization) can make the generated enum constant name diverge from the wire value even without a keyword collision. `toJson`/`fromJson` generation is mandatory on every generated type/input/enum/interface as of v5.0.0 — there is no config flag to suppress it. ## Queries All query, mutation, and subscription methods are `suspend` functions — call them from a coroutine or `runTest` in tests: ```kotlin import com.example.generated.client.GraphLinkClient import kotlinx.coroutines.runBlocking val user = runBlocking { client.queries.getUser("user-42").getUser } println("${user.name} — ${user.status}") ``` Compound queries that resolve multiple fields in a single network call return a typed response with one property per top-level resolver: ```kotlin val result = client.queries.fetchUserAndPost("user-1", "post-1") val user = result.user // User val post = result.post // Post ``` One HTTP call — two fully typed results. ## Mutations Input classes are plain data classes — construct them directly, no builder boilerplate: ```kotlin val input = CreateUserInput( name = "Alice Smith", email = "alice@example.com", status = UserStatus.ACTIVE, address = AddressInput("123 Main St", "Springfield", "US", "12345"), ) val response = client.mutations.createUser(input) println(response.createUser.id) // "user-1" ``` Nullable input fields simply have defaults — omit them when you don't need them: ```kotlin val patch = UpdateUserInput(status = UserStatus.INACTIVE) // only sets status client.mutations.updateUser("user-1", patch) ``` ## Subscriptions Subscriptions return a `Flow` backed by a WebSocket connection. Collect it in a coroutine: ```kotlin import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch val job = scope.launch { client.subscriptions.userCreated().collect { event -> println("New user: ${event.userCreated.name}") } } // cancel when done job.cancel() ``` Events are deserialized into the same typed response classes used by queries. ## Projected types When a query selects only a subset of a type's fields, GraphLink generates a dedicated projected class instead of reusing the full type. This ensures the Kotlin compiler catches field misuse at compile time: ```kotlin // schema query that selects only id, name, status val summary = client.queries.fetchUserSummary("user-1").getUser // summary is User_IdNameStatus — only .id, .name, .status are available println(summary.status) // UserStatus.ACTIVE ``` ## Union and interface types Union members are dispatched via `__typename`. Each member type implements the generated interface or sealed hierarchy: ```kotlin val results: List = client.queries.runSearch("alice").search for (result in results) { when (result) { is UserResult -> println("User: ${result.name}") is PostResult -> println("Post: ${result.title}") } } ``` ## Wiring the client ### Dependencies ```kotlin title="build.gradle.kts" dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("com.squareup.okhttp3:okhttp:4.12.0") // only if wsAdapter = "okhttp" } plugins { kotlin("plugin.serialization") version "2.0.0" } ``` ### HTTP adapter Implement the single-method `GraphLinkClientAdapter` interface with your HTTP client of choice. The interface is a Kotlin `fun interface`, so a lambda works too: ```kotlin import com.example.generated.interfaces.GraphLinkClientAdapter import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class OkHttpGraphLinkAdapter( private val url: String, private val httpClient: OkHttpClient = OkHttpClient(), private val headers: Map = emptyMap(), ) : GraphLinkClientAdapter { private val json = "application/json; charset=utf-8".toMediaType() override suspend fun execute(payload: String): String = withContext(Dispatchers.IO) { val request = Request.Builder() .url(url) .post(payload.toRequestBody(json)) .apply { headers.forEach { (k, v) -> addHeader(k, v) } } .build() httpClient.newCall(request).execute().use { response -> check(response.isSuccessful) { "HTTP ${response.code}" } response.body!!.string() } } } ``` ### WebSocket adapter (OkHttp) When `wsAdapter: "okhttp"` is set, GraphLink generates a default `DefaultGraphLinkWebSocketAdapter` that uses OkHttp. Pass your server URL and optional headers: ```kotlin import com.example.generated.client.DefaultGraphLinkWebSocketAdapter val wsAdapter = DefaultGraphLinkWebSocketAdapter( url = "wss://api.example.com/graphql", headers = mapOf("Authorization" to "Bearer $token"), ) ``` ### Assembling the client ```kotlin import com.example.generated.client.GraphLinkClient import com.example.generated.client.KotlinxSerializationGraphLinkJsonCodec val codec = KotlinxSerializationGraphLinkJsonCodec() val adapter = OkHttpGraphLinkAdapter("https://api.example.com/graphql", headers = mapOf("Authorization" to "Bearer $token")) val wsAdapter = DefaultGraphLinkWebSocketAdapter("wss://api.example.com/graphql", headers = mapOf("Authorization" to "Bearer $token")) val client = GraphLinkClient( adapter = adapter, wsAdapter = wsAdapter, multipartAdapter = NoOpMultipartAdapter(), // or a real upload adapter encoder = codec, decoder = codec, ) // Use it val user = runBlocking { client.queries.getUser("42").getUser } ``` ## Caching Cache behaviour is declared in the schema, not in code: ```graphql type Query { getUser(id: ID!): User! @glCache(ttl: "5m", tags: ["users"]) } type Mutation { updateUser(id: ID!, input: UpdateUserInput!): User! @glCacheInvalidate(tags: ["users"]) } ``` The generated client handles caching transparently — the second call to `getUser` with the same arguments is served from the in-memory cache; `updateUser` evicts it: ```kotlin val first = client.queries.getUser("1") // hits the network val second = client.queries.getUser("1") // served from cache — no HTTP call client.mutations.updateUser("1", UpdateUserInput(status = UserStatus.INACTIVE)) val third = client.queries.getUser("1") // cache evicted — hits the network again ``` Bring your own cache store by implementing `GraphLinkCacheStore` and passing it to the `GraphLinkClient` constructor. ## Inline error handling (`@glCaptureErrors`) Enable per-operation inline errors with the directive or globally via config: ```graphql type Query { findUser(id: ID!): User @glCaptureErrors } ``` ```kotlin // returns FindUserFullResponse with nullable data + nullable errors val result = client.queries.findUser("user-99") if (result.errors != null) { result.errors.forEach { println(it.message) } } else { println(result.data?.findUser?.name) } ``` Enable globally in config with `"captureErrors": true` under `clientConfig.kotlin`. ## Configuration reference | Option | Type | Default | Description | |---|---|---|---| | `packageName` | `string` | — | **Required.** Kotlin package name for all generated files. | | `generateAllFieldsFragments` | `boolean` | `true` | Generate `_all_fields_*` fragments for every type. | | `autoGenerateQueries` | `boolean` | `true` | Auto-generate query strings for all operations. | | `typeAsDataClass` | `boolean` | `true` | Emit output types as `data class` instead of `open class`. | | `inputAsDataClass` | `boolean` | `true` | Emit input types as `data class` instead of `open class`. | | `nullableFieldsRequired` | `boolean` | `false` | When `true`, nullable constructor parameters have no default. | | `immutableTypeFields` | `boolean` | `true` | Generate `val` fields (immutable) instead of `var`. | | `operationNameAsParameter` | `boolean` | `false` | Append the operation name as a `?operationName=` query parameter in the request URL. | | `captureErrors` | `boolean` | `false` | Return `FullResponse` (data + errors) for all operations instead of throwing. | | `wsAdapter` | `"okhttp"` \| `"none"` | `"okhttp"` | WebSocket adapter to generate. `"okhttp"` emits `DefaultGraphLinkWebSocketAdapter`; `"none"` emits only the interface. | | `defaultAlias` | `string?` | `null` | Default alias prefix for auto-generated query fields. | --- # Swift Client A fully typed GraphQL client generated from your schema — idiomatic Swift with `async`/`await`, `Sendable` structs, and no third-party dependency. `URLSession` (Foundation) is the only transport used, so the generated code compiles unchanged on Apple platforms and on Linux (swift-corelibs-foundation). ## Configuration Set `"mode": "client"` and add a `"swift"` key under `clientConfig`: === "JSON" ```json title="glink.json" { "schemaPaths": ["schema/*.graphql"], "mode": "client", "typeMappings": { "ID": "String", "String": "String", "Float": "Double", "Int": "Int", "Boolean": "Bool" }, "outputDir": "Sources/GraphLinkGenerated", "clientConfig": { "swift": { "moduleName": "GraphLinkGenerated", "generateAllFieldsFragments": true, "autoGenerateQueries": true, "immutableTypeFields": true, "wsAdapter": "urlsession" } } } ``` === "YAML" ```yaml title="glink.yaml" schemaPaths: - schema/*.graphql mode: client typeMappings: ID: String String: String Float: Double Int: Int Boolean: Bool outputDir: Sources/GraphLinkGenerated clientConfig: swift: moduleName: GraphLinkGenerated generateAllFieldsFragments: true autoGenerateQueries: true immutableTypeFields: true wsAdapter: urlsession ``` ## Generated output For a schema with `User`, `Post`, `CreateUserInput`, and a `UserStatus` enum, GraphLink generates: ``` Sources/GraphLinkGenerated/ Client/ GraphLinkClient.swift ← main client (queries / mutations / subscriptions) GraphLinkQueries.swift GraphLinkMutations.swift GraphLinkSubscriptions.swift GraphLinkClientAdapter.swift ← HTTP adapter typealias DefaultGraphLinkURLSessionWebSocketAdapter.swift GraphLinkMultipartAdapter.swift ← file-upload adapter typealias GraphLinkCacheStore.swift ← cache store protocol InMemoryGraphLinkCacheStore.swift ← actor-based in-memory cache GraphLinkCacheEntry.swift GraphLinkTagEntry.swift GraphLinkPartialQuery.swift GraphLinkException.swift GLUpload.swift ← file upload type ... Types/ User.swift Post.swift CreateUserResponse.swift ... Inputs/ CreateUserInput.swift Enums/ UserStatus.swift Interfaces/ SearchResult.swift ← generated union/interface protocol ``` ## Structs Types and inputs are generated as `Sendable` structs with an explicit memberwise `init` and `toJson()`/`fromJson()`: ```swift // generated public struct User: Sendable, Identifiable { public let id: String public let name: String public let email: String? public let status: UserStatus public let address: Address public init( id: String, name: String, email: String? = nil, status: UserStatus, address: Address ) { self.id = id self.name = name self.email = email self.status = status self.address = address } public func toJson() -> [String: Any?] { return [ "id": id, "name": name, "email": email, "status": status.toJson(), "address": address.toJson(), ] } public static func fromJson(_ map: [String: Any?]) -> User { return User( id: map["id"] as! String, name: map["name"] as! String, email: map["email"] as? String, status: UserStatus.fromJson(map["status"] as! String), address: Address.fromJson(map["address"] as! [String: Any?]) ) } } ``` A type is additionally `Identifiable` whenever it has a non-null `id` field — ready to drop into a SwiftUI `List` without extra glue. ## Enums ```swift // generated public enum UserStatus: String, Sendable { case active = "ACTIVE" case inactive = "INACTIVE" case suspended = "SUSPENDED" public func toJson() -> String { return rawValue } public static func fromJson(_ value: String) -> UserStatus { return UserStatus(rawValue: value)! } } ``` An unrecognized wire value force-unwraps to a crash, matching the throwing behavior of the Kotlin/Java/TypeScript enum `fromJson` — treat an unknown enum value from the server as a programmer/contract error, not a value to silently swallow. ## Queries All query, mutation, and subscription methods are `async throws` — call them from an `async` context: ```swift import GraphLinkGenerated let user = try await client.queries.getUser(id: "user-42").getUser print("\(user.name) — \(user.status)") ``` Compound queries that resolve multiple fields in a single network call return a typed response with one property per top-level resolver: ```swift let result = try await client.queries.fetchUserAndPost(userId: "user-1", postId: "post-1") let user = result.user // User let post = result.post // Post ``` One HTTP call — two fully typed results. ## Mutations Input structs are plain memberwise-initialized `Sendable` structs — construct them directly, no builder boilerplate: ```swift let input = CreateUserInput( name: "Alice Smith", email: "alice@example.com", status: .active, address: AddressInput(street: "123 Main St", city: "Springfield", country: "US", zip: "12345") ) let response = try await client.mutations.createUser(input: input) print(response.createUser.id) // "user-1" ``` Nullable input fields default to `nil` — omit them when you don't need them: ```swift let patch = UpdateUserInput(status: .inactive) // only sets status try await client.mutations.updateUser(id: "user-1", input: patch) ``` ## Subscriptions Subscriptions return an `AsyncThrowingStream` backed by a WebSocket connection. Iterate it with `for try await`: ```swift let task = Task { for try await event in client.subscriptions.userCreated() { print("New user: \(event.userCreated.name)") } } // cancel when done task.cancel() ``` Events are deserialized into the same typed response structs used by queries. ## Projected types When a query selects only a subset of a type's fields, GraphLink generates a dedicated projected struct instead of reusing the full type, so the Swift compiler catches field misuse at compile time: ```swift // schema query that selects only id, name, status let summary = try await client.queries.fetchUserSummary(id: "user-1").getUser // summary is User_IdNameStatus — only .id, .name, .status are available print(summary.status) // UserStatus.active ``` ## Union and interface types Union members are dispatched via `__typename`. Each member type conforms to the generated protocol: ```swift let results: [SearchResult] = try await client.queries.runSearch(query: "alice").search for result in results { switch result { case let user as UserResult: print("User: \(user.name)") case let post as PostResult: print("Post: \(post.title)") default: break } } ``` ## Wiring the client ### HTTP adapter `GraphLinkClientAdapter` is a plain closure typealias — implement it with `URLSession` or any HTTP client of your choice: ```swift import GraphLinkGenerated import Foundation let adapter: GraphLinkClientAdapter = { payload in var request = URLRequest(url: URL(string: "https://api.example.com/graphql")!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.httpBody = payload let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, 200..<300 ~= http.statusCode else { throw GraphLinkException.http } return data } ``` ### WebSocket adapter (URLSession) When `wsAdapter: "urlsession"` is set (the default), GraphLink generates a default `DefaultGraphLinkURLSessionWebSocketAdapter` built on `URLSessionWebSocketTask`. Pass your server URL and optional headers: ```swift import GraphLinkGenerated let wsAdapter = DefaultGraphLinkURLSessionWebSocketAdapter( url: URL(string: "wss://api.example.com/graphql")!, headers: ["Authorization": "Bearer \(token)"] ) ``` ### Assembling the client ```swift import GraphLinkGenerated let client = GraphLinkClient( adapter: adapter, wsAdapter: wsAdapter, multipartAdapter: DefaultGraphLinkURLSessionMultipartAdapter() ) // Use it let user = try await client.queries.getUser(id: "42").getUser ``` ## Caching Cache behaviour is declared in the schema, not in code: ```graphql type Query { getUser(id: ID!): User! @glCache(ttl: "5m", tags: ["users"]) } type Mutation { updateUser(id: ID!, input: UpdateUserInput!): User! @glCacheInvalidate(tags: ["users"]) } ``` The generated client handles caching transparently through an `actor`-based `InMemoryGraphLinkCacheStore` — the second call to `getUser` with the same arguments is served from the cache; `updateUser` evicts it: ```swift let first = try await client.queries.getUser(id: "1") // hits the network let second = try await client.queries.getUser(id: "1") // served from cache — no HTTP call try await client.mutations.updateUser(id: "1", input: UpdateUserInput(status: .inactive)) let third = try await client.queries.getUser(id: "1") // cache evicted — hits the network again ``` Bring your own cache store by conforming to `GraphLinkCacheStore` (a `Sendable` protocol) and passing it to the `GraphLinkClient` initializer. ## Inline error handling (`@glCaptureErrors`) Enable per-operation inline errors with the directive or globally via config: ```graphql type Query { findUser(id: ID!): User @glCaptureErrors } ``` ```swift // returns FindUserFullResponse with an optional data + optional errors let result = try await client.queries.findUser(id: "user-99") if let errors = result.errors { errors.forEach { print($0.message) } } else { print(result.data?.findUser?.name ?? "") } ``` Enable globally in config with `"captureErrors": true` under `clientConfig.swift`. ## File uploads Upload mutations accept a `GLUpload` value (raw `Data` plus filename/MIME type) and are sent through `DefaultGraphLinkURLSessionMultipartAdapter` per the GraphQL multipart request spec: ```swift let upload = GLUpload(data: fileData, filename: "avatar.png", mimeType: "image/png") try await client.mutations.uploadAvatar(userId: "1", file: upload) ``` Pass an `UploadProgressCallback` to the generated method to observe upload progress. ## Configuration reference | Option | Type | Default | Description | |---|---|---|---| | `moduleName` | `string` | — | **Required.** Used in doc comments / generated `Package.swift` (if emitted) — generated files have no module declaration of their own. | | `generateAllFieldsFragments` | `boolean` | `true` | Generate `_all_fields_*` fragments for every type. | | `autoGenerateQueries` | `boolean` | `true` | Auto-generate query strings for all operations. | | `nullableFieldsRequired` | `boolean` | `false` | When `true`, nullable initializer parameters have no default. | | `immutableTypeFields` | `boolean` | `true` | Generate `let` fields (immutable) instead of `var`. | | `operationNameAsParameter` | `boolean` | `false` | Append the operation name as a `?operationName=` query parameter in the request URL. | | `captureErrors` | `boolean` | `false` | Return `FullResponse` (data + errors) for all operations instead of throwing. | | `wsAdapter` | `"urlsession"` \| `"none"` | `"urlsession"` | WebSocket adapter to generate. `"urlsession"` emits `DefaultGraphLinkURLSessionWebSocketAdapter`; `"none"` emits only the adapter typealias. | | `defaultAlias` | `string?` | `null` | Default alias prefix for auto-generated query fields. | | `autoGenerateQueriesFor` | `object?` | `null` | Restrict auto-generated operations to an explicit allow-list per operation type. | | `autoGenerateQueriesArgumentLimit` | `int?` | `200` | Caps propagated field arguments per auto-generated operation before it's skipped with a warning. | | `maxFragmentBodySize` | `int?` | `8192` | Skips oversized auto-generated `_all_fields` fragments (and dependent auto-queries). Set to `null` to disable. | --- # Spring Boot Server GraphLink generates the entire Spring Boot scaffolding from your schema — controllers, service interfaces, types, inputs, and enums. ## Server mode config Set `"mode": "server"` and provide a `"spring"` section under `serverConfig`. The key options: === "JSON" ```json title="glink.json" { "schemaPaths": ["schema/*.graphql"], "mode": "server", "typeMappings": { "ID": "String", "String": "String", "Float": "Double", "Int": "Integer", "Boolean": "Boolean", "Null": "null" }, "outputDir": "src/main/java/com/example/generated", "serverConfig": { "spring": { "basePackage": "com.example.generated", "generateControllers": true, "generateRepositories": false, "immutableInputFields": true, "immutableTypeFields": false } } } ``` === "YAML" ```yaml title="glink.yaml" schemaPaths: - schema/*.graphql mode: server typeMappings: ID: String String: String Float: Double Int: Integer Boolean: Boolean Null: null outputDir: src/main/java/com/example/generated serverConfig: spring: basePackage: com.example.generated generateControllers: true generateRepositories: false immutableInputFields: true immutableTypeFields: false ``` | Option | Description | |---|---| | `generateControllers` | Generates `@Controller` classes with `@QueryMapping`, `@MutationMapping`, `@SubscriptionMapping`, and `@Argument` on parameters. | | `generateRepositories` | When `true`, generates JPA `Repository` interfaces for types annotated with `@glRepository`. | | `immutableInputFields` | Input class fields are `final`. Recommended: `true`. | | `immutableTypeFields` | Type class fields are `final`. Set to `false` for Spring Boot — Spring's GraphQL runtime sets fields via setters. | ## Java records Set `"inputAsRecord": true` and/or `"typeAsRecord": true` to generate inputs and types as Java records instead of mutable classes with setters. Records are more concise and enforce immutability at the language level: === "JSON" ```json title="glink.json — records" { "serverConfig": { "spring": { "basePackage": "com.example.generated", "inputAsRecord": true, "typeAsRecord": false } } } ``` === "YAML" ```yaml title="glink.yaml — records" serverConfig: spring: basePackage: com.example.generated inputAsRecord: true typeAsRecord: false ``` ## Schema generation Set `"generateSchema": true` to write the processed schema file to disk alongside the generated Java. This is useful when your Spring Boot app needs to serve the schema file at a specific path: === "JSON" ```json title="glink.json — schema generation" { "serverConfig": { "spring": { "basePackage": "com.example.generated", "generateSchema": true, "schemaTargetPath": "src/main/resources/graphql/schema.graphqls" } } } ``` === "YAML" ```yaml title="glink.yaml — schema generation" serverConfig: spring: basePackage: com.example.generated generateSchema: true schemaTargetPath: src/main/resources/graphql/schema.graphqls ``` `schemaTargetPath` must end in `.graphql` or `.graphqls`. The generated schema reflects all directive processing — internal directives like `@glSkipOnServer` are not emitted. ## Strict server generation Since v5.0.0, server generation is **strict by default**. Every generated type and interface enforces the schema's real nullability on its getters, constructors, and setters — a `title: String!` field is generated as non-null, not the always-nullable model used before v5.0.0. Generated controllers serialize their responses via `toJson()` to a `Map` at the controller boundary, so a resolver that can only return a partially-fetched object (for example, a repository query that selects just the columns present in the GraphQL selection set) doesn't need every non-null field populated. **Opting a type into partial/lenient population** — annotate it with `@glServerLenient`. All of its own fields are generated as nullable, so a resolver can construct it with un-fetched fields left `null`: ```graphql type LegacyReport @glServerLenient { id: ID! total: Float! } ``` !!! danger "Migration from pre-5.0.0" If your existing resolvers relied on server-generated types being fully nullable (e.g. constructing a partially-populated `Vehicle` with some fields left `null`), annotate those types with `@glServerLenient` before upgrading. ## What gets generated For the example schema, the generator produces 9 files: ``` src/main/java/com/example/generated/ controllers/ PersonServiceController.java ← generated, never touch VehicleServiceController.java ← generated, never touch services/ PersonService.java ← implement this VehicleService.java ← implement this types/ Person.java Vehicle.java inputs/ AddPersonInput.java AddVehicleInput.java enums/ FuelType.java ``` Controllers are generated and never touched by hand. Service interfaces are what you implement. Types, inputs, and enums are data classes. ## Types and inputs Server-side types are mutable — they have getters and setters, not final fields. This is required because Spring's GraphQL runtime deserializes JSON into these classes using reflection. ```java title="generated/types/Vehicle.java — server version (mutable)" public class Vehicle { private String id; private String brand; private String model; private Integer year; private FuelType fuelType; private String ownerId; public Vehicle() {} public String getId() { return id; } public void setId(String id) { this.id = id; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public String getModel() { return model; } public void setModel(String model) { this.model = model; } public Integer getYear() { return year; } public void setYear(Integer year) { this.year = year; } public FuelType getFuelType() { return fuelType; } public void setFuelType(FuelType fuelType) { this.fuelType = fuelType; } public String getOwnerId() { return ownerId; } public void setOwnerId(String ownerId) { this.ownerId = ownerId; } } ``` Input classes can be immutable since Spring maps query arguments into them at the framework level using constructors or builders. Note that `immutableTypeFields: false` applies to `type` definitions only; input classes follow `immutableInputFields`. ## Service interfaces For each group of operations sharing a root type, GraphLink generates one service interface: ```java title="generated/services/VehicleService.java" public interface VehicleService { Vehicle getVehicle(String id); List listVehicles(); Vehicle addVehicle(AddVehicleInput input); Flux vehicleAdded(); } ``` Observe the return types: - Queries return the domain type directly — `Vehicle`, not `Optional` or `Mono` - Subscriptions return `Flux` — a Project Reactor reactive stream - The method signatures exactly mirror the schema declarations You implement this interface and annotate your implementation with `@Service`. You do not touch the generated controller. ## Controllers The generated controller is the glue between Spring's GraphQL runtime and your service. It is fully annotated and delegates every call to the service interface. You never need to modify it. !!! warning "v5.0.0: controllers return `Map` / `List`, not typed objects" Since v5.0.0, generated Java Spring, Kotlin Spring, and Express/Apollo controllers serialize the service's result via the type's generated `toJson()` at the controller boundary, and return `Map` / `List>` instead of the typed domain object. **Service interfaces are unaffected** — you still implement them with typed parameters and typed return values. Only the controller's own return type (and therefore anything that inspects a controller method's return type directly, e.g. custom AOP or tests that call the controller method in isolation) needs to account for the change after regenerating. Identifier normalization and reserved-keyword renaming mean a field's generated identifier can differ from its wire name, so serializing through `toJson()` at the boundary keeps the wire payload correct even when the Java/Kotlin field name doesn't match. Java Spring controller methods also now bind input arguments as `Map` and convert them via the input's generated `fromJson()`, instead of receiving a Spring-bound typed input object directly — again to keep renamed/normalized fields correct on the wire. Any custom Jackson/Spring binding configuration relying on the previous direct-typed argument binding needs to be revisited. ```java title="generated/controllers/VehicleServiceController.java — v5.0.0+" @Controller() public class VehicleServiceController { private final VehicleService vehicleService; public VehicleServiceController(VehicleService vehicleService) { this.vehicleService = vehicleService; } @QueryMapping() public Map getVehicle(@Argument() String id) { return vehicleService.getVehicle(id).toJson(); } @QueryMapping() public List> listVehicles() { return vehicleService.listVehicles().stream().map(Vehicle::toJson).toList(); } @MutationMapping() public Map addVehicle(@Argument() Map input) { return vehicleService.addVehicle(AddVehicleInput.fromJson(input)).toJson(); } @SubscriptionMapping() public Flux> vehicleAdded() { return vehicleService.vehicleAdded().map(Vehicle::toJson); } } ``` Spring's `@QueryMapping`, `@MutationMapping`, and `@SubscriptionMapping` use the method name to map to the schema field by convention. `@Argument` on method parameters maps GraphQL arguments to Java parameters by name. ## Implementing the service Create a `@Service` class in your own package (not in the `generated` package) that implements the generated interface: ```java title="com/example/service/VehicleServiceImpl.java — your code" package com.example.service; import com.example.generated.services.VehicleService; import com.example.generated.types.Vehicle; import com.example.generated.inputs.AddVehicleInput; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Sinks; import java.util.List; @Service public class VehicleServiceImpl implements VehicleService { private final VehicleRepository vehicleRepository; private final Sinks.Many vehicleSink = Sinks.many().multicast().onBackpressureBuffer(); public VehicleServiceImpl(VehicleRepository vehicleRepository) { this.vehicleRepository = vehicleRepository; } @Override public Vehicle getVehicle(String id) { return vehicleRepository.findById(id).orElse(null); } @Override public List listVehicles() { return vehicleRepository.findAll(); } @Override public Vehicle addVehicle(AddVehicleInput input) { Vehicle v = new Vehicle(); v.setBrand(input.getBrand()); v.setModel(input.getModel()); v.setYear(input.getYear()); v.setFuelType(input.getFuelType()); v.setOwnerId(input.getOwnerId()); Vehicle saved = vehicleRepository.save(v); vehicleSink.tryEmitNext(saved); return saved; } @Override public Flux vehicleAdded() { return vehicleSink.asFlux(); } } ``` !!! info "Keep generated code separate" Put your implementations in a separate package from the generated code (e.g. `com.example.service` vs `com.example.generated`). This way, re-running the generator never overwrites your business logic. ## Subscriptions with Reactor Spring Boot GraphQL uses Project Reactor for subscriptions. The service interface returns `Flux` — a reactive stream that emits items over time. The recommended approach is `Sinks.Many`: a thread-safe construct that lets you push items from anywhere in your application: ```java title="Push-based subscription with Sinks" // Declare a multicast sink — supports multiple concurrent subscribers private final Sinks.Many vehicleSink = Sinks.many().multicast().onBackpressureBuffer(); // In vehicleAdded() — return the flux backed by the sink @Override public Flux vehicleAdded() { return vehicleSink.asFlux(); } // When a new vehicle is saved, push it to all subscribers vehicleSink.tryEmitNext(savedVehicle); // When the application shuts down (optional) vehicleSink.tryEmitComplete(); ``` `Sinks.many().multicast()` allows multiple GraphQL subscribers to receive the same events simultaneously. ## Reactive mode (Spring WebFlux) Set `"reactive": true` in `serverConfig.spring` to generate Spring WebFlux-style controllers. Queries and mutations return `Mono` instead of `T` directly, and subscriptions return `Flux` as usual. File upload fields use `FilePart` instead of `MultipartFile`. === "JSON" ```json title="glink.json — reactive mode" { "serverConfig": { "spring": { "basePackage": "com.example.generated", "reactive": true } } } ``` === "YAML" ```yaml title="glink.yaml — reactive mode" serverConfig: spring: basePackage: com.example.generated reactive: true ``` With `reactive: true`, the generated service interface returns reactive types: ```java title="Generated VehicleService.java — reactive mode" import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public interface VehicleService { Mono getVehicle(String id); Flux listVehicles(); Mono addVehicle(AddVehicleInput input); Flux vehicleAdded(); } ``` The generated controllers delegate to these reactive methods directly. Spring WebFlux handles back-pressure and non-blocking I/O automatically. ## Security context propagation (MVC) In MVC (non-reactive) mode, Spring Security's `SecurityContextHolder` is thread-local. When a controller delegates to an async `CompletableFuture`, the security context is not automatically carried to the worker thread. Enable `"useSpringSecurity": true` to have GraphLink capture the context before entering the future and restore it on the worker thread: === "JSON" ```json title="glink.json — security context propagation" { "serverConfig": { "spring": { "useSpringSecurity": true } } } ``` === "YAML" ```yaml title="glink.yaml — security context propagation" serverConfig: spring: useSpringSecurity: true ``` When enabled, the generated controller looks like this: ```java title="Generated controller — with useSpringSecurity: true" @QueryMapping() public CompletableFuture getVehicle(@Argument() String id) { SecurityContext ctx = SecurityContextHolder.getContext(); return CompletableFuture.supplyAsync(() -> { SecurityContextHolder.setContext(ctx); try { return vehicleService.getVehicle(id); } finally { SecurityContextHolder.clearContext(); } }); } ``` This option has no effect in reactive mode — Spring Security provides dedicated reactive support via `ReactiveSecurityContextHolder`. ## Forward mappings When a type is annotated with `@glSkipOnServer(mapTo: "SomeSchemaType")`, `mapTo` names another **type or interface declared in your schema** (not a Java class name). GraphLink resolves the skipped type to that type's generated class and automatically forwards fields that exist verbatim on it — same name and compatible type — without generating a service method or `@SchemaMapping` for them. Only fields absent from the target type still get full delegation. ```graphql title="Forward mapping example" type Vehicle { id: ID! brand: String! } # Client-facing view model — resolved on the server as a regular Vehicle, no VehicleCard class generated type VehicleCard @glSkipOnServer(mapTo: "Vehicle") { id: ID! # exists verbatim on Vehicle → auto-forwarded brand: String! # exists verbatim on Vehicle → auto-forwarded displayLabel: String! # not on Vehicle → @SchemaMapping + service method } ``` The `mapTo` target must exist in the schema and must not itself be marked `@glSkipOnServer`; otherwise generation fails with a validation error. Fields that match are resolved directly by Spring without any service delegation, which removes the boilerplate of writing `@SchemaMapping` methods for fields that already exist on the mapped type. ## File uploads When your schema uses the `Upload` scalar, Spring Boot controllers accept `MultipartFile` (MVC mode) or `FilePart` (reactive mode): ```graphql title="Schema with Upload scalar" scalar Upload type Mutation { uploadDocument(file: Upload!): String! } ``` === "MVC controller" ```java @MutationMapping() public String uploadDocument(@Argument() MultipartFile file) { return documentService.uploadDocument(file); } ``` === "Reactive controller" ```java @MutationMapping() public Mono uploadDocument(@Argument() FilePart file) { return documentService.uploadDocument(file); } ``` The service interface method receives the same type as the controller, so you implement it directly without any conversion. ## Validation with @glValidate Add `@glValidate` to a mutation in your schema to instruct GraphLink to generate a `validateX()` method in the service interface. The controller calls this method before the main method, giving you a place to throw validation exceptions before any business logic runs. ```graphql title="Schema with @glValidate" type Mutation { addVehicle(input: AddVehicleInput!): Vehicle! @glValidate } ``` With `@glValidate` on `addVehicle`, the generated service interface gains an extra method: ```java title="Generated VehicleService.java — with @glValidate" public interface VehicleService { // Called first by the controller — throw here to abort the mutation void validateAddVehicle(AddVehicleInput input); Vehicle addVehicle(AddVehicleInput input); List listVehicles(); Vehicle getVehicle(String id); Flux vehicleAdded(); } ``` The generated controller calls `validateAddVehicle` before `addVehicle`. In your implementation, throw any exception to abort: ```java title="Implementing the validation method" @Override public void validateAddVehicle(AddVehicleInput input) { if (input.getBrand() == null || input.getBrand().isBlank()) { throw new IllegalArgumentException("Brand must not be blank"); } if (input.getYear() < 1886 || input.getYear() > 2100) { throw new IllegalArgumentException("Year out of valid range"); } } @Override public Vehicle addVehicle(AddVehicleInput input) { // Only reached if validateAddVehicle did not throw // ... } ``` !!! note "@glValidate vs @glIntercept" `@glValidate` generates a dedicated `validateX(...)` method per mutation, typed to that mutation's own arguments — use it for mutation-specific input validation. `@glIntercept` generates one schema-wide `runBefore(tag, operation, args, context)` hook shared by every annotated field (queries, mutations, subscriptions, and schema/batch mappings alike) — use it for cross-cutting checks like auth. When both are present on the same field, `runBefore` fires first. → **[Full @glIntercept reference](directives.md#glintercept)** ## Kotlin Spring Boot server New in v5.0.0: set `"mode": "server"` and use a `"kotlinSpring"` section instead of `"spring"` under `serverConfig` to generate a Kotlin server target — data-class types/inputs/enums, services, controllers, and repositories, mirroring the Java Spring target above (including strict-by-default generation and `Map`-boundary controller serialization). === "JSON" ```json title="glink.json — Kotlin Spring server" { "schemaPaths": ["schema/*.graphql"], "mode": "server", "outputDir": "src/main/kotlin/com/example/generated", "serverConfig": { "kotlinSpring": { "basePackage": "com.example.generated", "typeAsDataClass": true, "inputAsDataClass": true, "blockingServices": true, "generateControllers": true, "generateRepositories": false } } } ``` === "YAML" ```yaml title="glink.yaml — Kotlin Spring server" schemaPaths: - schema/*.graphql mode: server outputDir: src/main/kotlin/com/example/generated serverConfig: kotlinSpring: basePackage: com.example.generated typeAsDataClass: true inputAsDataClass: true blockingServices: true generateControllers: true generateRepositories: false ``` | Option | Type | Default | Description | |---|---|---|---| | `typeAsDataClass` | `boolean` | `false` | Emit output types as `data class`. When `false`, uses `open class`. | | `inputAsDataClass` | `boolean` | `false` | Emit input types as `data class`. When `false`, uses `open class`. | | `blockingServices` | `boolean` | `true` | When `true`, controller methods wrap each service call in `withContext(Dispatchers.IO + SecurityCoroutineContext()) { ... }`, offloading blocking (e.g. JPA/JDBC) work off the coroutine dispatcher and propagating `SecurityContextHolder` across the switch. Set to `false` when your service layer is coroutine-native/non-blocking — methods are then emitted with no wrapping. | The remaining options (`generateControllers`, `generateRepositories`, `immutableInputFields`, `immutableTypeFields`, `generateSchema`, `schemaTargetPath`, `injectDataFetching`) behave the same as their Java Spring equivalents documented above. --- # Express / Apollo Server GraphLink generates a complete Express + Apollo Server scaffold from your schema — typed service interfaces, resolvers, WebSocket subscriptions, and (optionally) a ready-to-run HTTP server entry point. It mirrors the Spring Boot server model: you implement generated interfaces, GraphLink owns the wiring. ## Server mode config Set `"mode": "server"` and provide an `"expressApollo"` section under `serverConfig`: === "JSON" ```json title="glink.json" { "schemaPaths": ["schema/*.graphql"], "mode": "server", "typeMappings": { "ID": "string", "String": "string", "Float": "number", "Int": "number", "Boolean": "boolean", "Null": "null" }, "outputDir": "src/generated", "serverConfig": { "expressApollo": { "port": 4000, "graphqlPath": "/graphql", "generateEntryPoint": true } } } ``` === "YAML" ```yaml title="glink.yaml" schemaPaths: - schema/*.graphql mode: server typeMappings: ID: string String: string Float: number Int: number Boolean: boolean Null: null outputDir: src/generated serverConfig: expressApollo: port: 4000 graphqlPath: /graphql generateEntryPoint: true ``` | Option | Default | Description | |---|---|---| | `port` | `4000` | Port the generated entry point's HTTP server listens on. | | `graphqlPath` | `/graphql` | Path the Apollo middleware and the WebSocket server are mounted on. | | `generateEntryPoint` | `true` | Generate `src/generated/index.ts` with a ready-to-call `createServer(services)` function. Set to `false` if you want to wire Express/Apollo yourself and only use the generated types, resolvers, and service interfaces. | | `useResolveInfo` | `false` | Adds a `GraphQLResolveInfo` parameter to every generated service method, so resolvers can inspect the requested selection set (e.g. for partial-fetch optimizations). | | `injectContext` | `false` | Adds a `GraphLinkContext` parameter to every generated service method. Equivalent to putting `@glInjectContext` on every field — use that directive instead to opt in per-field. | ## What gets generated For a schema with `Article` and `Author` types: ``` src/generated/ index.ts ← generated, entry point (createServer) context.ts ← GraphLinkContext, GraphLinkLoaders typeDefs.ts ← the processed .graphql schema as a string resolvers/ build-resolvers.ts ← generated, never touch services/ article-service.ts ← implement this author-service.ts ← implement this types/ article.ts author.ts inputs/ create-article-input.ts enums/ article-type.ts ``` Resolvers are generated and never touched by hand. Service interfaces are what you implement — same split as the Spring Boot target. ## Service interfaces For each group of operations sharing a root type, GraphLink generates one service interface: ```typescript title="generated/services/article-service.ts" export interface ArticleService { getArticle(id: string): Promise
    ; listArticles(): Promise; createArticle(input: CreateArticleInput): Promise
    ; articleCreated(): AsyncIterable
    ; } ``` - Queries and mutations return `Promise` - Subscriptions return `AsyncIterable` — Apollo's native async-generator subscription model - Method signatures mirror the schema declarations exactly Implement the interface as a plain class: ```typescript title="src/impl/article-service.ts — your code" import { ArticleService } from '../generated/services/article-service.js'; import { Article } from '../generated/types/article.js'; import { CreateArticleInput } from '../generated/inputs/create-article-input.js'; export class ArticleServiceImpl implements ArticleService { async getArticle(id: string): Promise
    { return db.articles.findById(id); } async listArticles(): Promise { return db.articles.findAll(); } async createArticle(input: CreateArticleInput): Promise
    { return db.articles.insert(input); } async *articleCreated(): AsyncIterable
    { for await (const article of articleCreatedPubSub) { yield article; } } } ``` !!! info "Keep generated code separate" Put implementations under your own directory (e.g. `src/impl/`) — never inside `src/generated/`. Re-running the generator overwrites everything under `outputDir` unconditionally. ## The entry point — `createServer` With `generateEntryPoint: true` (the default), `index.ts` exports a `GraphLinkServices` interface (one property per generated service) and a `createServer(services)` function that wires Express, Apollo Server, and the `graphql-ws` WebSocket server together: ```typescript title="src/start.ts — your code" import { createServer } from './generated/index.js'; import { ArticleServiceImpl } from './impl/article-service.js'; import { AuthorServiceImpl } from './impl/author-service.js'; const httpServer = await createServer({ articleService: new ArticleServiceImpl(), authorService: new AuthorServiceImpl(), }); httpServer.listen(4000, () => { console.log('Server ready at http://localhost:4000/graphql'); }); ``` One class can satisfy multiple service slots — pass the same instance under several keys in the `services` object when it makes sense to group related operations behind one implementation. `createServer` returns a Node `http.Server` (not yet listening), so you can attach it to your own process lifecycle, health checks, or a graceful-shutdown hook before calling `.listen()`. ## Context and `contextFactory` `GraphLinkContext` starts as an empty extensible interface (`extends Record`). Add fields to it via TypeScript [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) in your own code, and populate them per-request with `contextFactory`: ```typescript title="src/impl/my-context.ts — your code" declare module '../generated/context.js' { interface GraphLinkContext { userId?: string; } } ``` ```typescript title="src/start.ts — populate context per request" const httpServer = await createServer({ // ...services contextFactory: async (req, res) => ({ userId: await getUserIdFromAuthHeader(req.headers.authorization), }), }); ``` `contextFactory` runs once per HTTP request (and once per WebSocket connection for subscriptions). Fields declared with `@glInjectContext`, or every field when `injectContext: true` is set, receive this context object as an argument. ## Batch mappings and DataLoader `@glSkipOnServer(batch: true)` fields (see [`@glSkipOnServer` & `@glSkipOnClient`](skip-directives.md)) generate a per-request [DataLoader](https://github.com/graphql/dataloader) instead of a Spring `@BatchMapping` handler — same N+1-avoidance guarantee, idiomatic to the Node ecosystem: ```typescript title="generated/loaders/author-schema-mappings-loaders.ts" export function createAuthorArticlesLoader( service: AuthorSchemaMappingsService, context: GraphLinkContext, ): DataLoader { return new DataLoader(async (authors) => service.articles(authors, context)); } ``` Loaders are constructed fresh per request inside `createServer`'s `context` function and attached to `context.loaders` — never shared across requests, avoiding the classic DataLoader cross-request cache-leak bug. ## Subscriptions Subscriptions use `graphql-ws` over a `WebSocketServer` mounted on the same HTTP server and path as the query/mutation endpoint. The generated service method is an `AsyncIterable`, most naturally implemented with an async generator backed by a pub/sub channel: ```typescript title="Push-based subscription with an async queue" import { PubSub } from 'graphql-subscriptions'; const pubsub = new PubSub(); async createArticle(input: CreateArticleInput): Promise
    { const article = await db.articles.insert(input); pubsub.publish('ARTICLE_CREATED', article); return article; } async *articleCreated(): AsyncIterable
    { for await (const { article } of pubsub.asyncIterator('ARTICLE_CREATED')) { yield article; } } ``` Any `AsyncIterable` works — `graphql-subscriptions`' `PubSub`, a hand-rolled async queue, or a message-broker-backed iterator all satisfy the generated interface. ## Responses serialize via `toJson()` Since v5.0.0, generated resolvers serialize the service's return value through the type's generated `toJson()` before handing it to Apollo, matching the Java/Kotlin Spring targets. This keeps the wire payload correct even when identifier normalization or keyword-safe renaming makes a generated TypeScript property name diverge from the GraphQL field name — you never need to think about it from inside your service implementation, which still returns plain typed objects. ## File uploads When your schema uses the `Upload` scalar, generated resolvers accept the [`graphql-upload`](https://github.com/jaydenseric/graphql-upload)-style `Promise` and your service method receives an already-resolved stream: ```graphql title="Schema with Upload scalar" scalar Upload type Mutation { uploadDocument(file: Upload!): String! } ``` ```typescript title="generated/services/document-service.ts" export interface DocumentService { uploadDocument(file: GLUpload): Promise; } ``` ## `@glIntercept` Add an `interceptor` service to run a shared pre-resolver check (authorization, rate limiting, ...) before any annotated query, mutation, subscription, or batch-mapped field executes: ```typescript title="Generated interfaces/graph-link-interceptor.ts" export interface GraphLinkInterceptor { runBefore( tag: GlInterceptorTag | null, operation: string, args: unknown[], context: GraphLinkContext | null, info: GraphQLResolveInfo | null, ): void; } ``` ```typescript title="src/impl/interceptor.ts — your code" export class InterceptorImpl implements GraphLinkInterceptor { runBefore(tag, operation, args, context) { if (tag === GlInterceptorTag.Admin && context?.userId !== ADMIN_ID) { throw new Error('Forbidden'); } } } ``` Pass it as `interceptor` on the `services` object passed to `createServer` — if the schema uses `@glIntercept` anywhere and no `interceptor` is provided, `createServer` throws immediately at startup instead of failing lazily on the first request. → **[Full `@glIntercept` reference](directives.md#glintercept)** ## Validation with `@glValidate` Exactly like the Spring Boot target: `@glValidate` on a mutation adds a `validateX(...)` method to the service interface, called by the generated resolver before the main method. Throw to abort the mutation before any business logic runs. See [Spring Boot Server → Validation with `@glValidate`](spring-server.md#validation-with-glvalidate) for the full write-up — the contract is identical, only the language differs. ## Development workflow ```bash glink -c config.json # regenerate src/generated/ npx tsc --noEmit # typecheck npx tsx src/start.ts # run ``` Regeneration only touches files under `outputDir` (`src/generated/` above) — your `src/impl/` and `src/start.ts` are never touched. --- # Custom Queries & Mutations GraphLink has supported hand-written GraphQL operation documents since day one. Write a query or mutation directly in your schema file (or in a separate `.graphql` file) and the generator produces a fully typed client method for it — same as auto-generated ones, with the same caching support. ## Where to write them Custom operations live in any file matched by `schemaPaths`. You can put them in the same file as the schema or split them into separate files: ``` schema/ schema.graphql ← types, queries, mutations, subscriptions operations.graphql ← your custom operation documents fragments.graphql ← shared fragments ``` ```json title="glink.json — picking up all files" { "schemaPaths": ["schema/*.graphql"] } ``` All files are merged before generation. GraphLink treats every `query`, `mutation`, and `subscription` block it finds as an operation to generate a client method for. --- ## Basic custom query Write a standard GraphQL operation document. Variable names, field selections, and nesting are all yours to define: ```graphql title="schema/operations.graphql" query fetchUserProfile($id: ID!) { getUser(id: $id) { id firstName lastName email } } ``` Generated Dart method: ```dart final res = await client.queries.fetchUserProfile(id: '42'); print(res.getUser.firstName); print(res.getUser.email); ``` The method name is derived from the operation name (`fetchUserProfile`). The response type is `FetchUserProfileResponse` with a typed `.getUser` field containing only the fields you projected. --- ## Using `_all_fields` Instead of listing every field manually, use `... _all_fields` to select all fields of the return type. GraphLink resolves it to the correct type-specific fragment automatically: ```graphql query fetchUser($id: ID!) { getUser(id: $id) { ... _all_fields } } ``` Or use the explicit form if you prefer: ```graphql query fetchUser($id: ID!) { getUser(id: $id) { ... _all_fields_User } } ``` Both are equivalent. `_all_fields` is resolved based on the return type of the field. --- ## Aliases and projections Any valid GraphQL field selection — with or without aliases — always generates a correctly named and typed response object. The generated field names match your aliases exactly. There is no constraint on what you can project or how you name it. ```graphql query fetchCarAndOwner($carId: ID!, $ownerId: ID!) { vehicle: getCar(id: $carId) { ... _all_fields } owner: getUser(id: $ownerId) { firstName lastName } } ``` The generated response type has fields named after the aliases: ```dart final res = await client.queries.fetchCarAndOwner(carId: '1', ownerId: '2'); print(res.vehicle.make); // alias "vehicle" → getCar result print(res.owner.firstName); // alias "owner" → getUser result ``` Without an alias the field name matches the resolver name. With an alias it matches the alias — always: ```graphql query fetchSummary($id: ID!) { latestCar: getCar(id: $id) { make model } primaryOwner: getUser(id: $id) { firstName } count: getCarsCount } ``` ```dart final res = await client.queries.fetchSummary(id: '1'); res.latestCar.make; // named "latestCar" res.primaryOwner.firstName; // named "primaryOwner" res.count; // named "count", typed int ``` The projected type for each field is generated from the field selection you wrote. If you project `{ make model }` on a `Car`, GraphLink generates a projected type with exactly those two fields. If you project all fields (via `... _all_fields` or explicitly), it reuses the base type. --- ## Multiple resolvers in one query This is one of the most powerful features. A single custom query can call multiple resolvers and get back one unified, typed response: ```graphql title="Dashboard query — one round trip for everything" query fetchDashboard($userId: ID!) { profile: getUser(id: $userId) { ... _all_fields } cars: getCarsByUser(userId: $userId) { ... _all_fields } count: getNotificationsCount(userId: $userId) } ``` ```dart title="One call, everything typed" final res = await client.queries.fetchDashboard(userId: session.userId); print(res.profile.firstName); print(res.cars.length); print(res.count); ``` The server receives a single GraphQL request. The response type `FetchDashboardResponse` has a typed field for each resolver in the query. --- ## Custom field projection You control exactly which fields come back. GraphLink generates a dedicated projected type matching your selection — not the full schema type: ```graphql title="Lean projection — only what the screen needs" query fetchCarList($page: PageInput!) { getCars(page: $page) { id make model year } } ``` The response contains only `id`, `make`, `model`, and `year`. GraphLink generates a projected type (e.g. `Car_IdMakeModelYear`) that holds exactly those fields. Nullability from the schema is preserved on each field. When a projection selects **all** fields of a type, GraphLink is smart enough to reuse the base type directly — no duplicate class is generated. --- ## Custom mutations Custom mutations follow the same pattern. Select exactly which fields you want from the response: ```graphql title="Custom mutation — specific response fields" mutation registerCar($input: CreateCarInput!) { createCar(input: $input) { id make model } } ``` ```dart final res = await client.mutations.registerCar( input: CreateCarInput(make: 'Toyota', model: 'Camry', year: 2024), ); print(res.createCar.id); ``` --- ## Shared fragments Define a fragment once and reuse it across multiple operations: ```graphql title="schema/fragments.graphql" fragment CarSummary on Car { id make model year } ``` ```graphql title="schema/operations.graphql" query fetchCar($id: ID!) { getCar(id: $id) { ... CarSummary } } query fetchCarsAndOwner($ownerId: ID!) { getCars(ownerId: $ownerId) { ... CarSummary } getUser(id: $ownerId) { firstName lastName } } ``` GraphLink resolves fragment spreads and inlines the selected fields into the generated query strings. Your fragments do not need to be in the same file as the operations that use them — any file in `schemaPaths` is visible to all others. --- ## Selective auto-generation (`autoGenerateQueriesFor`) `autoGenerateQueries: true` is convenient for small or medium schemas, but on large ones it can produce hundreds of client methods and megabytes of generated code you'll never use. `autoGenerateQueriesFor` solves this: list only the operations you actually need, and GraphLink generates methods exclusively for those. ```json title="glink.json — generate only what you need" { "clientConfig": { "dart": { "generateAllFieldsFragments": true, "autoGenerateQueriesFor": { "queries": ["getUser", "listOrders", "getDashboard"], "mutations": ["createOrder", "cancelOrder"], "subscriptions": ["onOrderUpdated"] } } } } ``` ```yaml title="glink.yaml — same in YAML" clientConfig: dart: generateAllFieldsFragments: true autoGenerateQueriesFor: queries: - getUser - listOrders - getDashboard mutations: - createOrder - cancelOrder subscriptions: - onOrderUpdated ``` The three keys — `queries`, `mutations`, `subscriptions` — are independent. You can list operations under any combination of them; keys you omit (or set to `[]`) produce no auto-generated methods for that operation type. **Build error on unknown names.** If a name you list doesn't exist as a root field in the schema, GraphLink aborts with a descriptive error: ``` autoGenerateQueriesFor.queries lists unknown operation(s): getDashbord ``` This catches typos at generation time rather than at runtime. **Works alongside manual queries.** `autoGenerateQueriesFor` only controls what gets auto-generated from root fields. Any operation document you write by hand in a `.graphql` file is always generated regardless of this setting — the two approaches compose freely. **Requires `generateAllFieldsFragments: true`**, because auto-generation uses the `_all_fields` fragments to build query bodies. --- ## Auto-generated query argument limit When `autoGenerateQueries` is on, GraphLink propagates the arguments of every nested field up to the operation level. For most schemas the number of propagated arguments is small — they are grouped automatically into a single `fieldArgs` object so the method signature stays clean. On very large schemas (e.g. GitHub's `node` query), the `fieldArgs` object can grow to hundreds of fields, producing an object that is unrealistic to construct by hand and difficult to compile. `autoGenerateQueriesArgumentLimit` (default `200`) is a safety cap: if an auto-generated operation's `fieldArgs` input would contain more fields than the limit, GraphLink **skips** that operation entirely and prints a `⚠` warning: ``` ⚠ Auto-generated query "node" skipped: 1219 propagated args exceed the limit of 200. Write a custom query with a narrower selection, or raise autoGenerateQueriesArgumentLimit in your config. ``` **Hand-written operations are never affected** — the cap applies only to operations that GraphLink generates automatically. ### What to do when an operation is skipped **Option 1 — write a custom query.** This is the recommended approach for large operations. Select only the fields you actually need: ```graphql title="schema/operations.graphql — narrow selection instead of _all_fields" query fetchNode($id: ID!) { node(id: $id) { ... on Repository { id name description } } } ``` **Option 2 — use `autoGenerateQueriesFor`** to auto-generate the operation but restrict which operations are generated overall, reducing total output without raising the limit. **Option 3 — raise the limit.** If the generated `fieldArgs` object is genuinely useful in your case, increase the cap: ```json title="glink.json" { "clientConfig": { "dart": { "autoGenerateQueriesArgumentLimit": 500 } } } ``` --- ## Client-side default values (v5.0.0+) GraphQL `= value` defaults on input fields and on query/mutation arguments are now emitted as real defaults in generated Dart, Java, Kotlin, and TypeScript code — previously they were parsed but silently dropped. Swift (added in v5.1.0) supports the same behavior. ```graphql title="Schema with default values" input PageInput { size: Int = 20 offset: Int = 0 } type Query { listVehicles(page: PageInput = { size: 20, offset: 0 }, sortDesc: Boolean = false): [Vehicle!]! } ``` ```dart title="Generated Dart — real defaults, const where possible" const PageInput({this.size = 20, this.offset = 0}); Future listVehicles({ PageInput page = const PageInput(), bool sortDesc = false, }) async { /* ... */ } ``` Callers can omit `page` and `sortDesc` entirely and get the schema's declared defaults. The generated `.graphqls` schema output also preserves the `= value` syntax when `generateSchema: true` is set. --- ## Auto-generated query variable names (v5.0.0+) Auto-generated operation variable names changed from bare `$` to `$` (with a further type-suffix appended only if the same field+arg name resolves to different types across the schema). This avoids variable collisions across fields and fragments selected in the same auto-generated operation. ```graphql title="Before v5.0.0" query getVehicle($id: ID!) { getVehicle(id: $id) { ... _all_fields } } ``` ```graphql title="v5.0.0+ — namespaced by field name" query getVehicle($getVehicleId: ID!) { getVehicle(id: $getVehicleId) { ... _all_fields } } ``` This only affects **auto-generated** operations (`autoGenerateQueries` / `autoGenerateQueriesFor`) — the wire-level `.graphql` query string changes, but the generated client method's parameter name is unaffected and behavior is equivalent. Hand-written operations keep whatever variable names you wrote. --- ## Caching on custom queries `@glCache` and `@glCacheInvalidate` work exactly the same on custom operations. Apply them at the operation level or per resolver field: ```graphql title="Custom query with caching" query fetchDashboard($userId: ID!, $page: PageInput!) @glCache(ttl: "5m", tags: ["dashboard"]) { profile: getUser(id: $userId) @glCache(ttl: "10m", tags: ["users"]) { ... _all_fields } cars: getCarsByUser(userId: $userId) @glCache(ttl: "2m", tags: ["cars"]) { ... _all_fields } } ``` Each resolver is cached independently. If `"cars"` is invalidated, only that resolver is re-fetched on the next call — `profile` stays warm. See [Caching](caching.md) for the full caching reference. --- ## What gets generated For every custom operation, GraphLink generates: | Item | Naming convention | Example | |---|---|---| | Client method | camelCase of operation name | `fetchDashboard(...)` | | Response type | `{OperationName}Response` | `FetchDashboardResponse` | | Method parameters | one per `$variable` | `userId`, `page` | | Response fields | one per resolver / alias | `.profile`, `.cars`, `.count` | The method lives under `client.queries`, `client.mutations`, or `client.subscriptions` depending on the operation type. Everything is fully typed — no casting, no dynamic maps. --- ## What most tools get wrong Most GraphQL code generators fall apart on three things: **interfaces**, **unions**, and **subscriptions**. GraphLink handles all three, including when they are combined. ### Interfaces and inline fragments Given this schema: ```graphql interface Animal { id: ID! name: String! } type Dog implements Animal { id: ID! name: String! breed: String! } type Cat implements Animal { id: ID! name: String! indoor: Boolean! } type Query { getAnimal: Animal! } ``` Write an inline fragment query projecting different fields per concrete type: ```graphql query fetchAnimal { getAnimal { ... on Dog { id name breed } ... on Cat { id name indoor } } } ``` GraphLink generates: - An **abstract base class** `Animal_` with a `fromJson` factory that dispatches on `__typename` - Concrete classes `Dog_IdNameBreed` and `Cat_IdNameIndoor`, each implementing `Animal_` - `FetchAnimalResponse` with `getAnimal` typed as `Animal_` ```dart title="Usage — fully typed, no casting" final res = await client.queries.fetchAnimal(); final animal = res.getAnimal; switch (animal) { case Dog_IdNameBreed dog: print('Dog: ${dog.breed}'); case Cat_IdNameIndoor cat: print('Cat, indoor: ${cat.indoor}'); } ``` The compiler knows every possible concrete type. No `Map`. No manual `__typename` checks. No casting. --- ### Unions Unions work the same way. Each member type gets a concrete class implementing the generated abstract union base: ```graphql union SearchResult = Car | Owner | Driver type Query { search(term: String!): [SearchResult!]! } query runSearch($term: String!) { search(term: $term) { ... on Car { id make model } ... on Owner { id name email } ... on Driver { id name licenseNumber } } } ``` Generated abstract class `SearchResult_` with concrete `Car_IdMakeModel`, `Owner_IdNameEmail`, `Driver_IdNameLicenseNumber` — all dispatched from `fromJson` based on `__typename`. The `search` field on the response is `List` — fully typed, no generics needed at the call site. --- ### Subscriptions Subscriptions generate a `Stream` of the typed response — not `Stream`, not `Stream>`: ```graphql type Subscription { newNotification(userId: ID!): Notification! } subscription watchNotifications($userId: ID!) { newNotification(userId: $userId) { id message createdAt } } ``` ```dart title="Typed subscription stream" client.subscriptions.watchNotifications(userId: session.userId).listen((event) { // event is WatchNotificationsResponse — fully typed print(event.newNotification.message); print(event.newNotification.createdAt); }); ``` The generated WebSocket adapter handles the `graphql-ws` protocol, connection init, ping/pong, and exponential-backoff reconnect automatically. --- ### All three together The real test: a subscription that returns a **union type**, requiring inline fragments to distinguish the concrete type at runtime. Most tools give up here. GraphLink generates it correctly: ```graphql union FeedEvent = NewMessage | UserJoined | UserLeft type Subscription { feedEvents(roomId: ID!): FeedEvent! } subscription watchFeed($roomId: ID!) { feedEvents(roomId: $roomId) { ... on NewMessage { authorName text sentAt } ... on UserJoined { username joinedAt } ... on UserLeft { username leftAt reason } } } ``` ```dart title="Typed stream of a union — no boilerplate" client.subscriptions.watchFeed(roomId: room.id).listen((event) { final feed = event.feedEvents; // typed as FeedEvent_ switch (feed) { case NewMessage_AuthorNameTextSentAt msg: chatLog.add('[${msg.authorName}] ${msg.text}'); case UserJoined_UsernameJoinedAt join: chatLog.add('${join.username} joined'); case UserLeft_UsernameLeftAtReason left: chatLog.add('${left.username} left: ${left.reason}'); } }); ``` Every event is dispatched by `__typename`, every concrete type is fully typed, and the stream reconnects automatically on disconnect. This is what most GraphQL tools describe as "coming soon." --- # Built-In Caching Cache control belongs in the schema, not scattered across your application code. ## How it works GraphLink caching is opt-in and declared entirely at the schema level using two directives: `@glCache` on queries and `@glCacheInvalidate` on mutations. The generated client handles all cache logic automatically — reading from cache, writing to cache, and evicting entries on mutation. No application code is needed to enable or configure caching. The cache operates on the client side. It stores serialized response data keyed by operation name + variables. The server never knows about client-side caching. ## @glCache — caching a query Annotate any query field with `@glCache` to have its response cached: ```graphql title="@glCache directive" type Query { # Cache for 2 minutes, tagged "vehicles" getVehicle(id: ID!): Vehicle! @glCache(ttl: "2m", tags: ["vehicles"]) # Cache for 1 minute, same tag group listVehicles: [Vehicle!]! @glCache(ttl: "1m", tags: ["vehicles"]) # Cache for 5 minutes, no tags (cannot be tag-invalidated) getStaticConfig: AppConfig! @glCache(ttl: "5m") # Cache with staleIfOffline — serve stale data if network fails getUserProfile(id: ID!): UserProfile @glCache(ttl: "1m", staleIfOffline: true) } ``` | Argument | Type | Required | Description | |---|---|---|---| | `ttl` | `String` | Yes | Time-to-live as a duration string. After this duration, the entry is expired and the next call hits the server. | | `tags` | `[String!]` | No | List of tags to associate with this cache entry. Used for group invalidation via `@glCacheInvalidate`. | | `staleIfOffline` | `Boolean` | No | If `true`, serve the expired cached value when the network call fails instead of throwing. | **TTL duration formats:** | Format | Unit | Example | Equivalent | |--------|------|---------|------------| | `"s"` | seconds | `"90s"` | 90 seconds | | `"m"` | minutes | `"4m"` | 240 seconds | | `"h"` | hours | `"2h"` | 7 200 seconds | | `"d"` | days | `"1d"` | 86 400 seconds | | `""` | seconds (bare) | `"300"` | 300 seconds | ## Cache keys Cache entries are keyed by a combination of the operation name and the serialized variables. The key is computed as an FNV1a hash of `operationName + JSON(variables)`. This means: - `getVehicle("42")` and `getVehicle("99")` produce different cache entries - `getVehicle("42")` called twice produces the same cache key — the second call returns the cached result - `listVehicles()` (no variables) always produces the same cache key regardless of call site !!! info "Per-argument caching" Each unique argument combination gets its own cache entry. Calling `getVehicle` with 100 different IDs stores 100 separate cache entries. All of them share the `"vehicles"` tag, so a single `addVehicle` mutation invalidates all 100 at once. ## @glCacheInvalidate — busting the cache Annotate a mutation with `@glCacheInvalidate` to automatically evict cache entries when the mutation succeeds: ```graphql title="@glCacheInvalidate directive" type Mutation { # Evicts all cache entries tagged "vehicles" addVehicle(input: AddVehicleInput!): Vehicle! @glCacheInvalidate(tags: ["vehicles"]) # Evicts entries tagged "vehicles" AND "persons" transferVehicle(vehicleId: ID!, newOwnerId: ID!): Vehicle! @glCacheInvalidate(tags: ["vehicles", "persons"]) # Wipes the ENTIRE cache resetAllData: Boolean! @glCacheInvalidate(all: true) } ``` | Argument | Type | Description | |---|---|---| | `tags` | `[String!]` | Evict all cache entries that were stored with any of these tags. | | `all` | `Boolean` | When `true`, evict the entire cache regardless of tags. | ## Tag-based invalidation Tags are labels you attach to cache entries at write time. When you later invalidate a tag, all entries carrying that tag are evicted simultaneously. This allows a single mutation to invalidate many different cached queries at once, even if those queries have different operation names and different variable sets: ```graphql title="Tag invalidation in action" type Query { getVehicle(id: ID!): Vehicle! @glCache(ttl: "2m", tags: ["vehicles"]) listVehicles: [Vehicle!]! @glCache(ttl: "1m", tags: ["vehicles"]) getFleet(ownerId: ID!): [Vehicle!]! @glCache(ttl: "90s", tags: ["vehicles"]) } type Mutation { # One mutation invalidates ALL three queries above addVehicle(input: AddVehicleInput!): Vehicle! @glCacheInvalidate(tags: ["vehicles"]) updateVehicle(id: ID!, input: AddVehicleInput!): Vehicle! @glCacheInvalidate(tags: ["vehicles"]) deleteVehicle(id: ID!): Boolean! @glCacheInvalidate(tags: ["vehicles"]) } ``` After calling `addVehicle`, the next call to `getVehicle`, `listVehicles`, or `getFleet` will always go to the server — no stale data. ## Partial query caching This is GraphLink's most powerful caching feature. When a query returns multiple aliased fields, each field can carry its own `@glCache` directive. The client fetches each field independently and caches them separately. The result: if one tag is invalidated, only the affected portion of the query is re-fetched. The rest is still served from cache. ```graphql title="Partial caching — schema" type Query { # These two aliases will be cached separately vehicle: Vehicle! @glCache(ttl: "2m", tags: ["vehicles"]) owner: Person! @glCache(ttl: "5m", tags: ["persons"]) } ``` Here is what happens step by step: 1. **First call** — both `vehicle` and `owner` are cache misses. The client sends requests to the server for both. Results are stored: `vehicle` tagged `["vehicles"]`, `owner` tagged `["persons"]`. 2. **Second call (within TTL)** — both hits. No network requests. 3. **addVehicle mutation runs** — `@glCacheInvalidate(tags: ["vehicles"])` evicts the `vehicle` entry. The `owner` entry is unaffected. 4. **Third call** — `vehicle` is a miss (re-fetched from server). `owner` is still a hit (served from cache). One network request instead of two. Partial query caching is especially valuable in dashboard or profile screens that combine volatile data (frequently updated) with stable data (rarely changed). The stable portion never leaves the cache even when the volatile portion is invalidated. ## staleIfOffline When `staleIfOffline: true` is set on a cached query and the cache entry has expired, GraphLink attempts a server request. If that request fails (network error, timeout, server unreachable), it falls back to the expired cached value instead of throwing an exception. This is especially useful on mobile apps where network connectivity is unreliable. Users see slightly outdated data rather than an error screen: ```graphql title="staleIfOffline example" type Query { # If expired and network fails, return the last known value getUserProfile(id: ID!): UserProfile @glCache(ttl: "1m", staleIfOffline: true) # Without staleIfOffline — throws on network failure after TTL getVehicle(id: ID!): Vehicle! @glCache(ttl: "2m") } ``` ## Custom cache store The default cache store is `InMemoryGraphLinkCacheStore`. It is an in-process, non-persistent LRU map. For most apps this is sufficient. If you need persistence, cross-process sharing, or custom eviction, implement the `GraphLinkCacheStore` interface: ```java title="GraphLinkCacheStore interface (generated)" public interface GraphLinkCacheStore { // Store an entry with its TTL (in seconds) and tags void set(String key, Object value, int ttl, List tags); // Retrieve a cached entry, or null if expired/absent Object get(String key); // Evict all entries matching any of the given tags void invalidate(List tags); // Evict all entries in the store void invalidateAll(); } ``` Examples of custom implementations you might write: - **SharedPreferences (Flutter)** — persist cache across app restarts on mobile - **Redis-backed (Java)** — share cache across multiple service instances - **Encrypted store** — encrypt sensitive data at rest on device - **Size-limited LRU** — evict least-recently-used entries when a size cap is reached Pass the custom store to the client constructor (the `store:` named parameter in Dart, the last argument in Java): === "Dart" ```dart final client = GraphLinkClient( adapter: myHttpAdapterFn, wsAdapter: DefaultGraphLinkWebSocketAdapter(url: 'ws://localhost:8080/graphql'), store: MyCustomCacheStore(), // implements GraphLinkCacheStore ); ``` === "Java" ```java GraphLinkClient client = new GraphLinkClient( adapter, encoder, decoder, new MyRedisCacheStore(redisClient) // implements GraphLinkCacheStore ); ``` ## Dart usage — full cache flow ```dart title="Cache flow in Dart" // 1. First call — cache miss, hits the server // Result stored with key=hash("getVehicle"+"42"), ttl=120s, tags=["vehicles"] final res1 = await client.queries.getVehicle(id: '42'); print(res1.getVehicle.brand); // Toyota — from server // 2. Second call within 120s — cache hit, no network request final res2 = await client.queries.getVehicle(id: '42'); print(res2.getVehicle.brand); // Toyota — from cache // 3. Different ID — separate cache entry final res3 = await client.queries.getVehicle(id: '99'); print(res3.getVehicle.brand); // Honda — from server (different cache key) // 4. Mutation — @glCacheInvalidate(tags: ["vehicles"]) evicts all "vehicles" entries await client.mutations.addVehicle( input: AddVehicleInput(brand: 'Ford', model: 'Focus', year: 2024, fuelType: FuelType.GASOLINE), ); // 5. After mutation — cache miss again for all "vehicles" entries final res5 = await client.queries.getVehicle(id: '42'); print(res5.getVehicle.brand); // Toyota — from server again (cache was cleared) ``` ## Java usage — full cache flow ```java title="Cache flow in Java" // 1. First call — cache miss, hits the server GetVehicleResponse res1 = client.queries.getVehicle("42"); System.out.println(res1.getGetVehicle().getBrand()); // Toyota — from server // 2. Second call within 120s — cache hit, no network GetVehicleResponse res2 = client.queries.getVehicle("42"); System.out.println(res2.getGetVehicle().getBrand()); // Toyota — from cache // 3. List query — its own cache entry (ttl=60, tags=["vehicles"]) ListVehiclesResponse list1 = client.queries.listVehicles(); System.out.println(list1.getListVehicles().size()); // 3 — from server // 4. Mutation — evicts all "vehicles" cache entries client.mutations.addVehicle( AddVehicleInput.builder() .brand("Ford").model("Focus").year(2024) .fuelType(FuelType.GASOLINE).build() ); // 5. After mutation — getVehicle("42") and listVehicles() both miss cache GetVehicleResponse res5 = client.queries.getVehicle("42"); System.out.println(res5.getGetVehicle().getBrand()); // Toyota — from server again ListVehiclesResponse list2 = client.queries.listVehicles(); System.out.println(list2.getListVehicles().size()); // 4 — from server (includes new Ford) ``` --- # Directives Reference All GraphLink directives with arguments, placement, and examples. ## Copy-paste declarations Add this block to your schema file (or a dedicated `directives.graphql`) so your IDE and schema validators recognise every GraphLink directive: ```graphql title="directives.graphql" # ── Caching ─────────────────────────────────────────────────────────────────── directive @glCache( ttl: String! tags: [String!] staleIfOffline: Boolean ) on QUERY | MUTATION | FIELD | FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR | SCHEMA directive @glCacheInvalidate( tags: [String!] all: Boolean ) on QUERY | MUTATION | FIELD | FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR | SCHEMA # ── Code generation control ─────────────────────────────────────────────────── directive @glSkipOnServer(mapTo: String, batch: Boolean) on OBJECT | SCALAR | FIELD_DEFINITION directive @glSkipOnClient on OBJECT | INPUT_OBJECT | SCALAR | FIELD_DEFINITION directive @glExternal(glClass: String!, glImport: String) on SCALAR | OBJECT directive @glInternal on OBJECT directive @glTypeName(name: String!) on OBJECT | INPUT_OBJECT | ENUM # ── Server (Spring Boot) ────────────────────────────────────────────────────── directive @glDecorators(value: [String!]!) on OBJECT | INPUT_OBJECT | FIELD_DEFINITION directive @glServiceName(name: String!) on OBJECT | FIELD_DEFINITION directive @glValidate on FIELD_DEFINITION directive @glRepository(glType: String!, glIdType: String!) on OBJECT # ── Equality ────────────────────────────────────────────────────────────────── directive @glEqualsHashcode(fields: [String!]!) on OBJECT | INPUT_OBJECT # ── Input mapping ───────────────────────────────────────────────────────────── directive @glMapsTo(type: String!) on INPUT_OBJECT directive @glMapField(to: String!) on FIELD_DEFINITION # ── File upload ─────────────────────────────────────────────────────────────── directive @glUpload on SCALAR # ── Cyclic expansion ────────────────────────────────────────────────────────── directive @glExpand(depth: Int) on OBJECT # ── Error capture ───────────────────────────────────────────────────────────── directive @glCaptureErrors on FIELD_DEFINITION # ── Strict server generation (v5.0.0+) ──────────────────────────────────────── directive @glServerLenient on OBJECT | INTERFACE directive @glInjectContext on FIELD_DEFINITION directive @glIntercept(tag: String) on QUERY | MUTATION | SUBSCRIPTION | FIELD_DEFINITION | OBJECT ``` --- ## @glCache **Target:** CLIENT · **Placement:** `FIELD_DEFINITION` on `Query` fields Caches the result of a query field. The generated client checks the cache before making a network request and stores the result on a cache miss. | Argument | Type | Required | Description | |---|---|---|---| | `ttl` | `String!` | Yes | Time-to-live as a duration string (`"30s"`, `"4m"`, `"2h"`, `"1d"`) or a bare integer string (`"300"` = 300 seconds). | | `tags` | `[String!]` | No | Tags to associate with this cache entry for group invalidation. | | `staleIfOffline` | `Boolean` | No | When `true`, return the expired cached value if the network request fails. | **TTL duration formats:** | Format | Unit | Example | Seconds | |--------|------|---------|---------| | `"s"` | seconds | `"90s"` | 90 | | `"m"` | minutes | `"4m"` | 240 | | `"h"` | hours | `"2h"` | 7200 | | `"d"` | days | `"1d"` | 86400 | | `""` | seconds (bare) | `"300"` | 300 | ```graphql title="Example" type Query { getVehicle(id: ID!): Vehicle! @glCache(ttl: "2m", tags: ["vehicles"]) getUserProfile(id: ID!): UserProfile @glCache(ttl: "1m", tags: ["users"], staleIfOffline: true) getConfig: AppConfig! @glCache(ttl: "1h") } ``` --- ## @glCacheInvalidate **Target:** CLIENT · **Placement:** `FIELD_DEFINITION` on `Mutation` fields Invalidates cache entries after a successful mutation. Either specify `tags` to evict by tag, or set `all: true` to wipe the entire cache. | Argument | Type | Description | |---|---|---| | `tags` | `[String!]` | Evict all entries tagged with any of these values. | | `all` | `Boolean` | When `true`, evict the entire cache store. | ```graphql title="Example" type Mutation { addVehicle(input: AddVehicleInput!): Vehicle! @glCacheInvalidate(tags: ["vehicles"]) updatePerson(input: UpdatePersonInput!): Person! @glCacheInvalidate(tags: ["persons", "vehicles"]) resetDatabase: Boolean! @glCacheInvalidate(all: true) } ``` --- ## @glCaptureErrors **Target:** CLIENT · **Placement:** `FIELD_DEFINITION` on `Query` and `Mutation` fields Changes the method contract so errors are returned inline instead of thrown. The generated method return type changes from `{OperationName}Response` to `{OperationName}FullResponse`. A separate `{OperationName}FullResponse` class is generated alongside the original response class (which is left unchanged). The caller inspects `response.errors` directly — the method never throws for GraphQL errors. No arguments. ```graphql title="Example" type Query { getUser(id: ID!): User! @glCaptureErrors listUsers: [User!]! @glCaptureErrors } type Mutation { createUser(input: CreateUserInput!): User! @glCaptureErrors } ``` Generated wrapper class (Dart): ```dart title="Generated GetUserFullResponse" class GetUserFullResponse { final GetUserResponse? data; final List? errors; GetUserFullResponse({this.data, this.errors}); } ``` Generated wrapper interface (TypeScript): ```typescript title="Generated GetUserFullResponse" export interface GetUserFullResponse { data?: GetUserResponse; errors?: GraphLinkError[]; } ``` Generated wrapper class (Java): ```java title="Generated GetUserFullResponse.java" public class GetUserFullResponse { private final GetUserResponse data; // nullable private final List errors; // nullable } ``` **Call-site comparison:** | Without `@glCaptureErrors` | With `@glCaptureErrors` | |---|---| | `try { final user = await client.queries.getUser(id: id); } catch (e) { … }` | `final r = await client.queries.getUser(id: id); if (r.errors != null) { … }` | **Caching interaction:** error responses are never cached. On a cache hit, `errors` is always `null`. **Validation:** applying `@glCaptureErrors` to a subscription field is rejected at parse time with a `ParseException`. **Global config alternative:** instead of annotating each field, set `captureErrors: true` in `clientConfig.dart` / `clientConfig.java` / `clientConfig.typescript` to apply the behaviour to every query and mutation automatically. --- ## @glServerLenient **Target:** SERVER · **Placement:** `OBJECT`, `INTERFACE` Since v5.0.0, server generation is **strict by default**: every generated type and interface enforces the schema's real nullability (a non-null field stays non-null in the generated getters/constructors/setters). `@glServerLenient` opts a specific type or interface back out of that, restoring the pre-v5.0.0 behavior where all of its own fields are generated as nullable — useful for legacy resolvers that can't guarantee every field is populated on every path. No arguments. ```graphql title="Example" # Every field on LegacyReport is generated as nullable, regardless of the schema's "!" markers type LegacyReport @glServerLenient { id: ID! title: String! total: Float! } ``` Because generated controllers serialize responses via `toJson()` to a `Map`, a `@glServerLenient` resolver can return a partially-populated object with un-fetched fields left `null` — enough for selection-driven partial fetches. → **[Full explanation of strict server generation](spring-server.md#strict-server-generation)** --- ## @glInjectContext **Target:** SERVER · **Placement:** `FIELD_DEFINITION` on `Query`, `Mutation`, `Subscription` fields, or a `@glSkipOnServer` relation field Injects the request **context** into that one resolver/service method so it can read per-request state (authenticated user, tenant, headers, etc.). The generated method gains the target's context parameter — Spring `GraphQLContext graphQLContext`, Apollo `context: GraphLinkContext`. Without it, the method has no context parameter. No arguments. Equivalent to enabling the global `injectContext` config for just that field; the directive and the global flag are OR'd together. ```graphql title="Example" type Query { # Only this resolver receives the request context me: User! @glInjectContext publicStats: Stats! } ``` ```java title="Generated UserService.java (Spring)" User me(GraphQLContext graphQLContext); Stats publicStats(); ``` ```typescript title="Generated UserService.ts (Apollo)" me(context: GraphLinkContext): Promise; publicStats(): Promise; ``` To inject context into **every** resolver instead, set `injectContext: true` in the server config (`serverConfig.spring`, `serverConfig.kotlinSpring`, or `serverConfig.expressApollo`). --- ## @glIntercept **Target:** SERVER · **Placement:** `QUERY`, `MUTATION`, `SUBSCRIPTION` root fields, `FIELD_DEFINITION` on a `@glSkipOnServer` schema/batch mapping field, or an `OBJECT`/`extend type` block (applies to every field declared in that block) A generic pre-resolver interceptor hook, resolver-agnostic — it doesn't know or care what the resolver does. GraphLink generates one `GraphLinkInterceptor` interface (and, if any `tag:` argument is used anywhere in the schema, a `GlInterceptorTag` enum) per server project. You provide exactly one implementation as a bean/service; the generated controller calls `runBefore(...)` before the resolver body runs on every field the directive covers. **Throw to deny, return to proceed** — there is no separate allow/deny return value. | Argument | Type | Required | Description | |---|---|---|---| | `tag` | `String` | No | An arbitrary label describing the check to run (e.g. `"auth"`, `"admin"`). Collected across the whole schema into a generated `GlInterceptorTag` enum, so `runBefore` can `switch`/`when` on it instead of comparing raw strings. Omit it for a bare `@glIntercept` with no tag. | ```graphql title="Example" type Query { me: User! @glIntercept(tag: "auth") publicStats: Stats! } type Mutation { deleteUser(id: ID!): Boolean! @glIntercept(tag: "admin") } type Subscription { orderUpdates: Order! @glIntercept(tag: "auth") } # Applies @glIntercept(tag: "auth") to every field declared in this block extend type Query @glIntercept(tag: "auth") { adminReport: Report! } ``` `runBefore` receives the resolver's own declared arguments (in schema declaration order, before any input-mapping/`fromJson` conversion), the request context, and — for a root field — the resolver's `GraphQLResolveInfo`/`DataFetchingEnvironment` when the target's `useResolveInfo`/`injectDataFetching` config is enabled: ```typescript title="Generated GraphLinkInterceptor.ts (Express/Apollo)" export interface GraphLinkInterceptor { runBefore(tag: GlInterceptorTag | null, operation: string, args: unknown[], context: GraphLinkContext | null, info: GraphQLResolveInfo | null): void; } ``` ```java title="Generated GraphLinkInterceptor.java (Spring)" public interface GraphLinkInterceptor { void runBefore(GlInterceptorTag tag, String operation, List args, GraphQLContext context); } ``` ```kotlin title="Generated GraphLinkInterceptor.kt (Spring)" interface GraphLinkInterceptor { suspend fun runBefore(tag: GlInterceptorTag?, operation: String, args: List, context: GraphQLContext): Unit } ``` `operation` is the root field's name (e.g. `"me"`) or, for a schema/batch mapping, `"{Type}.{field}"` (e.g. `"Team.members"`) — there is no root query/mutation/subscription name to reuse for a mapping. Provide your implementation as a Spring bean (`@Component`) or (Express/Apollo) as `interceptor` on the services object passed to the generated server bootstrap — a missing bean fails fast at startup/construction when the schema uses `@glIntercept` anywhere. ```java title="Example runBefore implementation" @Component public class InterceptorImpl implements GraphLinkInterceptor { @Override public void runBefore(GlInterceptorTag tag, String operation, List args, GraphQLContext context) { if (tag == GlInterceptorTag.AUTH && context.get("user") == null) { throw new RuntimeException("Access denied"); } } } ``` !!! warning "`@glIntercept` on a batch-mapped field with a real argument requires `batch: false`" `@glIntercept` works on a `@glSkipOnServer` mapping field exactly like it does on a root field — but on the JVM targets (Java/Kotlin Spring), Spring GraphQL's `@BatchMapping` handler method can **only** resolve the batch key collection, `GraphQLContext`, `@ContextValue`, `BatchLoaderEnvironment`, or `Principal` as parameters — never a real declared `@Argument`. If the field takes an argument (e.g. `members(role: String!)`), it must use `@glSkipOnServer(batch: false)` (a `@SchemaMapping`, resolved per-parent instead of batched) so the argument can be bound normally. A zero-argument batch-mapped field is unaffected and can freely use `batch: true` with `@glIntercept`. TypeScript's DataLoader-based batching has no such restriction — arguments are threaded through a per-argument-value memoized loader automatically regardless of `batch: true`/`false`. For a subscription, `runBefore` fires once at subscribe time — before the resolver hands back its stream — not on every emitted event. --- ## @glSkipOnServer **Target:** SERVER · **Placement:** `OBJECT`, `SCALAR`, `FIELD_DEFINITION` Excludes a type or field from server generation and generates `@SchemaMapping` or `@BatchMapping` to resolve it. Supports `mapTo` for substituting an existing class and `batch` for N+1-safe batch resolution. → **[Full reference with generated code examples](skip-directives.md)** --- ## @glSkipOnClient **Target:** BOTH · **Placement:** `OBJECT`, `INPUT_OBJECT`, `SCALAR`, `FIELD_DEFINITION` Enforces server-side access control at two levels. **Always**: generates a `@SchemaMapping` that throws `GraphQLException("Access denied")` — no client can read the field at runtime. **With `generateSchema: true`**: also strips the field from the schema Spring Boot serves, making it invisible in GraphiQL and rejected at validation before execution. Also excludes the type or field from all client-side code generation (Dart, Java, TypeScript, Kotlin, Swift). → **[Full reference including the server-side security guarantee](skip-directives.md)** --- ## @glExternal **Target:** BOTH · **Placement:** `SCALAR`, `OBJECT` Maps a GraphQL scalar or type to an external class, optionally specifying the import path. Unlike `typeMappings` in the config (which works for scalars globally), `@glExternal` is per-type and can specify an import statement so generated files get the correct import automatically. | Argument | Type | Description | |---|---|---| | `glClass` | `String!` | The fully-qualified class name to use. | | `glImport` | `String` | Optional import statement to add to generated files that reference this type. | ```graphql title="Example" # Map the DateTime scalar to Java's OffsetDateTime scalar DateTime @glExternal( glClass: "OffsetDateTime", glImport: "java.time.OffsetDateTime" ) # Map the BigDecimal scalar to Java's BigDecimal scalar BigDecimal @glExternal( glClass: "BigDecimal", glImport: "java.math.BigDecimal" ) ``` --- ## @glTypeName **Target:** CLIENT · **Placement:** `OBJECT`, `INPUT_OBJECT`, `ENUM` Overrides the name of the generated class for a type. By default, GraphLink uses the GraphQL type name. | Argument | Type | Description | |---|---|---| | `name` | `String!` | The class name to use in generated code. | ```graphql title="Example" # GraphQL type is "GQLVehicle", but generated class will be named "Vehicle" type GQLVehicle @glTypeName(name: "Vehicle") { id: ID! brand: String! } ``` --- ## @glDecorators **Target:** SERVER · **Placement:** `OBJECT`, `INPUT_OBJECT` Adds raw annotation strings to the generated class declaration. Useful for adding JPA annotations (`@Entity`, `@Table`), Lombok annotations, or any other annotation that belongs on the class. | Argument | Type | Description | |---|---|---| | `value` | `[String!]!` | List of annotation strings to emit before the class declaration. | ```graphql title="Example" type Vehicle @glDecorators(value: ["@Entity", "@Table(name = \"vehicles\")"]) { id: ID! brand: String! model: String! } ``` Generated output: ```java title="Generated Vehicle.java" @Entity @Table(name = "vehicles") public class Vehicle { // ... } ``` --- ## @glServiceName **Target:** SERVER · **Placement:** `OBJECT`, `FIELD_DEFINITION` Sets a custom name for the generated service interface associated with a type or operation. By default, the service is named `{TypeName}Service`. | Argument | Type | Description | |---|---|---| | `name` | `String!` | The service interface name to generate. | ```graphql title="Example" # Generates FleetManagementService instead of VehicleService type Vehicle @glServiceName(name: "FleetManagementService") { id: ID! brand: String! } ``` --- ## @glEqualsHashcode **Target:** BOTH · **Placement:** `OBJECT`, `INPUT_OBJECT` Generates `equals()` and `hashCode()` methods on the produced class, based on the specified fields. In Dart, generates `==` and `hashCode` overrides. In Java, generates standard `equals`/`hashCode` based on the listed fields. | Argument | Type | Description | |---|---|---| | `fields` | `[String!]!` | The field names to include in equality comparison. | ```graphql title="Example" # Two Vehicles are equal if they have the same id type Vehicle @glEqualsHashcode(fields: ["id"]) { id: ID! brand: String! model: String! } # Two AddVehicleInputs are equal if all fields match input AddVehicleInput @glEqualsHashcode(fields: ["brand", "model", "year", "fuelType"]) { brand: String! model: String! year: Int! fuelType: FuelType! } ``` --- ## @glRepository **Target:** SERVER · **Placement:** `OBJECT` Generates a JPA `JpaRepository` interface for this type. Requires `generateRepositories: true` in the server config. The repository is named `{TypeName}Repository`. | Argument | Type | Description | |---|---|---| | `glType` | `String!` | The entity class name. | | `glIdType` | `String!` | The Java type of the ID field (e.g. `"String"`, `"Long"`, `"UUID"`). | ```graphql title="Example" type Vehicle @glRepository(glType: "Vehicle", glIdType: "String") { id: ID! brand: String! } ``` Generated output (when `generateRepositories: true`): ```java title="Generated VehicleRepository.java" import org.springframework.data.jpa.repository.JpaRepository; public interface VehicleRepository extends JpaRepository { } ``` --- ## @glInternal **Target:** BOTH · **Placement:** `OBJECT` Marks a type as internal to the GraphLink runtime. Internal types are excluded from `_all_fields` fragment generation and from any UI widget generation. Use this for generated infrastructure types like error wrappers or pagination metadata that should not appear in user-facing code. ```graphql title="Example" # This type will not get an _all_fields fragment and will be skipped by UI generators type GraphLinkError @glInternal { message: String! locations: [GraphLinkErrorLocation] path: [String] } ``` --- ## @glValidate **Target:** SERVER · **Placement:** `FIELD_DEFINITION` on `Mutation` fields Generates a `validate{OperationName}()` method in the service interface. The generated controller calls this method before the main operation method. Throw any exception in the validate method to abort the mutation. ```graphql title="Example" type Mutation { addVehicle(input: AddVehicleInput!): Vehicle! @glValidate } ``` Generated service interface additions: ```java title="Generated VehicleService.java" public interface VehicleService { // Called before addVehicle — throw to reject void validateAddVehicle(AddVehicleInput input); Vehicle addVehicle(AddVehicleInput input); // ... } ``` --- ## @glArray — Removed in v4.4.0 !!! danger "@glArray has been removed" This directive is no longer supported. As of v4.4.0, all list fields are generated as `List` in Java and Dart regardless of this annotation. Remove any `@glArray` usages from your schema before upgrading to v4.4.0+. --- ## @glMapsTo **Target:** BOTH · **Placement:** `INPUT_OBJECT` Declares that a GraphQL input type maps to an existing class in the target language. GraphLink validates that the target class and all field mappings exist at generation time and emits a mapping constructor or method on the generated input class. Use `@glMapField` on individual fields to rename them during mapping. | Argument | Type | Description | |---|---|---| | `type` | `String!` | The fully-qualified target class name to map to. | ```graphql title="Example" # Map GraphQL input to an existing Java/Dart class input UpdateVehicleInput @glMapsTo(type: "com.example.domain.UpdateVehicleCommand") { id: ID! brand: String! model: String! year: Int! # Rename during mapping — see @glMapField below fuelKind: FuelType! @glMapField(to: "fuelType") } ``` GraphLink generates a `toMappedType()` method on the input class that constructs the target type from the input fields, applying any `@glMapField` renames in the process. --- ## @glMapField **Target:** BOTH · **Placement:** `FIELD_DEFINITION` on `input` types annotated with `@glMapsTo` Renames a field during the input-to-target mapping. The GraphQL field name is used in the schema and the generated input class; the `to` value is the field name on the target class. | Argument | Type | Description | |---|---|---| | `to` | `String!` | The field name on the target mapped class. | ```graphql title="Example" input UpdateVehicleInput @glMapsTo(type: "com.example.domain.UpdateVehicleCommand") { id: ID! brand: String! # GraphQL field is "fuelKind", target class field is "fuelType" fuelKind: FuelType! @glMapField(to: "fuelType") } ``` GraphLink validates at generation time that the `to` field name exists on the target class. If it does not, generation fails with a clear error message. --- ## @glUpload **Target:** BOTH · **Placement:** `SCALAR` Marks a scalar as a file upload type. In Dart, the scalar is mapped to `GLUpload`. In Java MVC, mutation arguments become `MultipartFile` (or `List`). In WebFlux, they become `FilePart` (or `List`). The generated client adapter handles multipart encoding automatically. ```graphql title="Example" scalar Upload @glUpload type Mutation { uploadAvatar(userId: ID!, file: Upload!): User! uploadDocuments(files: [Upload!]!): [Document!]! } ``` --- ## @glExpand **Target:** CLIENT · **Placement:** `OBJECT` Controls how cyclic type references are handled during `_all_fields` fragment generation. When a type references itself (directly or through a chain), GraphLink cannot emit a recursive fragment spread — it would be invalid GraphQL. Instead, it inlines the cyclic type's fields up to `depth` levels deep. | Argument | Type | Default | Description | |---|---|---|---| | `depth` | `Int` | `1` | Number of inline expansion levels for cyclic back-references. `0` omits the cyclic field entirely. | ```graphql title="Example — self-referential type" type Employee @glExpand(depth: 2) { id: ID! name: String! manager: Employee } ``` Generated fragment (depth 2 — two levels of inline expansion): ```graphql title="Generated _all_fields_Employee" fragment _all_fields_Employee on Employee { id name manager { id name manager { id name } } } ``` Without `@glExpand`, the default depth is `1` — one level of inline expansion. Use `depth: 0` to omit the cyclic field entirely from the fragment. !!! note "Multi-type cycles" `@glExpand` also handles indirect cycles — for example `Customer → Order → Product → Supplier → Customer`. Each type in the cycle should declare `@glExpand` if a non-default depth is needed. The cycle is detected automatically; `@glExpand` only controls how deep the inline expansion goes before truncating. !!! warning "Cyclic fields are always nullable (v5.0.0+)" Since v5.0.0, GraphLink runs SCC-based cycle detection by default on the generated *types themselves*, not just on `_all_fields` fragment expansion. Any field participating in a dependency cycle between types (e.g. `Employee.manager: Employee!` in a self-referential type) is generated as nullable in Dart/Java/Kotlin/TypeScript, even though the schema marks it non-null. Code that force-unwraps such a field (`!` in Dart/Kotlin, an unguarded getter call in Java/TS) may need updating after regenerating. `@glExpand` still controls only how deep `_all_fields` fragments inline the cycle — it does not opt a field back into non-null. --- ## @deprecated **Target:** BOTH · **Placement:** `FIELD_DEFINITION`, `ENUM_VALUE` (standard GraphQL directive) The spec-standard `@deprecated(reason: "...")` directive is picked up by GraphLink and translated into the target language's native deprecation annotation, carrying the reason through. ```graphql title="Example" type Vehicle { id: ID! brand: String! # Deprecated field fuelKind: FuelType @deprecated(reason: "Use `fuelType` instead") } enum FuelType { GASOLINE DIESEL ELECTRIC LEGACY_HYBRID @deprecated(reason: "Use HYBRID instead") } ``` | Target | Emitted annotation | |---|---| | Dart | `@Deprecated('Use \`fuelType\` instead')` | | Java / Kotlin | `@Deprecated` with the reason in a preceding Javadoc/KDoc `@deprecated` line | | TypeScript | `/** @deprecated Use \`fuelType\` instead */` JSDoc comment | | Swift | `@available(*, deprecated, message: "Use \`fuelType\` instead")` | --- ## _all_fields — the magic fragment When `generateAllFieldsFragments: true` is set in the config, GraphLink generates a named fragment for every type in the schema. The fragment selects all fields of that type and is named `_all_fields_{TypeName}`: ```graphql title="Generated _all_fields_Vehicle fragment" fragment _all_fields_Vehicle on Vehicle { id brand model year fuelType ownerId } ``` You can reference these fragments in hand-written queries as a shorthand: ```graphql title="Using _all_fields in a query" query getVehicle($id: ID!) { getVehicle(id: $id) { ... _all_fields_Vehicle } } # Or use the shorthand — resolves to the type-appropriate fragment query getVehicle($id: ID!) { getVehicle(id: $id) { ... _all_fields } } ``` The shorthand `... _all_fields` (without the type suffix) is resolved by GraphLink based on the return type of the field. It is equivalent to writing `... _all_fields_Vehicle` when the field returns `Vehicle`. The `autoGenerateQueries: true` config option uses these fragments internally to generate query strings for every operation in the schema — you never write query strings by hand at all. !!! warning "Types annotated with @glInternal are excluded" Internal types (annotated with `@glInternal`) do not get `_all_fields` fragments. This prevents GraphLink's own runtime types from being included in user queries. --- # @glSkipOnServer & @glSkipOnClient Two directives that give you fine-grained control over what GraphLink generates for each target. | Directive | Target | Effect | |---|---|---| | `@glSkipOnServer` | Server (Spring Boot) | Excludes a type or field from server generation; generates `@SchemaMapping` / `@BatchMapping` to resolve it | | `@glSkipOnClient` | Client (Dart, Java, TypeScript, Kotlin, Swift) | Strips the type or field from the generated schema — **no client can query it** | Declaration (add to your schema or a dedicated `directives.graphql`): ```graphql directive @glSkipOnServer(mapTo: String, batch: Boolean) on OBJECT | SCALAR | FIELD_DEFINITION directive @glSkipOnClient on OBJECT | INPUT_OBJECT | SCALAR | FIELD_DEFINITION ``` --- ## @glSkipOnClient is a server-side security guarantee !!! danger "This is not just client code generation" `@glSkipOnClient` enforces access control **on the server**, at two levels depending on whether `generateSchema` is enabled. In both cases, the field cannot be read by any client — not GraphiQL, not Postman, not a rogue app. GraphLink enforces `@glSkipOnClient` through two complementary mechanisms: ### Level 1 — Runtime rejection (always active) Even without any schema generation, GraphLink generates a `@SchemaMapping` handler for every `@glSkipOnClient` field that immediately throws a `GraphQLException`: ```graphql title="Schema" type User { id: ID! email: String! passwordHash: String! @glSkipOnClient } ``` ```java title="Generated VehicleServiceController.java (always generated)" // Any client that queries passwordHash receives a GraphQL error — access denied. @SchemaMapping(typeName="User", field="passwordHash") public CompletableFuture passwordHash(User value) { throw new GraphQLException("Access denied to field 'User.passwordHash'"); } ``` Any query that includes `passwordHash` receives a GraphQL error in the response. The field is still visible via introspection at this level — but it is completely inaccessible at runtime. ### Level 2 — Schema stripping (with `generateSchema: true`) When `generateSchema: true` is enabled, GraphLink also strips every `@glSkipOnClient` type, field, input, enum, and scalar from the schema file it writes to disk. Spring Boot serves this stripped schema as the authoritative GraphQL schema. ```graphql title="Your source schema" type User { id: ID! email: String! passwordHash: String! @glSkipOnClient # never exposed internalScore: Float! @glSkipOnClient # never exposed } ``` ```graphql title="Generated schema served by Spring Boot (generateSchema: true)" # passwordHash and internalScore are completely absent. # Spring rejects any query referencing them at validation — before execution. type User { id: ID! email: String! } ``` With the stripped schema in place: - The field does not appear in GraphiQL or any introspection result - Spring Boot rejects any query referencing it at the **validation layer**, before execution even starts — `400 Bad Request` - The `@SchemaMapping` exception handler from Level 1 is **not generated** for this field (it's not needed — the field doesn't exist in the schema) ### How to enable Level 2 Add `generateSchema: true` to your Spring Boot config: ```json title="glink.json" { "mode": "server", "serverConfig": { "spring": { "basePackage": "com.example.generated", "generateSchema": true, "schemaTargetPath": "src/main/resources/graphql/schema.graphqls" } } } ``` Run `glink` and the stripped schema lands at `src/main/resources/graphql/schema.graphqls`. Spring Boot picks it up automatically. ### Summary | | `generateSchema: false` | `generateSchema: true` | |---|---|---| | Field visible in GraphiQL | Yes | **No** | | Field accessible at runtime | **No** — throws `GraphQLException` | **No** — rejected at validation | | Rejection layer | Execution (runtime error) | Validation (`400 Bad Request`) | | `@SchemaMapping` generated | Yes — throws access denied | No — field stripped from schema | !!! tip "Use both" Level 1 is always active and requires no configuration. Enable Level 2 (`generateSchema: true`) for the strongest guarantee — fields vanish from introspection entirely and are rejected before execution. Use both together in production. --- --- ## @glSkipOnServer ### Applied to a type — no `mapTo` When placed on a type with no arguments, GraphLink skips generating a Java class for it. Any field on another type that returns this type is resolved via `@SchemaMapping`. The return type in service and controller signatures becomes `Object`. ```graphql title="Schema" type Vehicle { id: ID! brand: String! stats: VehicleStats! } type VehicleStats @glSkipOnServer { totalMileage: Int! lastServiceDate: String! } ``` === "MVC" ```java title="Generated VehicleService.java" public interface VehicleService { Vehicle getVehicle(String id); Object stats(Vehicle vehicle); } ``` ```java title="Generated VehicleServiceController.java" @Controller public class VehicleServiceController { @QueryMapping public CompletableFuture getVehicle(@Argument String id) { return CompletableFuture.supplyAsync(() -> vehicleService.getVehicle(id)); } // return type is Object — VehicleStats has no mapTo @SchemaMapping public CompletableFuture stats(Vehicle vehicle) { return CompletableFuture.supplyAsync(() -> vehicleService.stats(vehicle)); } } ``` === "WebFlux" ```java title="Generated VehicleService.java" public interface VehicleService { Mono getVehicle(String id); Mono stats(Vehicle vehicle); } ``` ```java title="Generated VehicleServiceController.java" @Controller public class VehicleServiceController { @QueryMapping public Mono getVehicle(@Argument String id) { return vehicleService.getVehicle(id); } @SchemaMapping public Mono stats(Vehicle vehicle) { return vehicleService.stats(vehicle); } } ``` --- ### Applied to a type — with `mapTo` `mapTo` takes a **GraphQL type name defined in your schema**. GraphLink resolves it to the generated class for that type and uses it as the return type wherever the skipped type appears. GraphLink also compares the fields of the two types. Fields that exist verbatim on the target type (same name and compatible type) are **auto-forwarded** — Spring resolves them directly from the mapped object, so no service method or `@SchemaMapping` is generated for them. Only fields absent from the target type get full delegation. ```graphql title="Schema" type Vehicle { id: ID! brand: String! model: String! } type Query { searchVehicles(query: String!): [VehicleCard!]! } # Client-facing view model. On the server, resolve as a regular Vehicle — # no VehicleCard class is generated. type VehicleCard @glSkipOnServer(mapTo: "Vehicle") { id: ID! # same field on Vehicle → auto-forwarded brand: String! # same field on Vehicle → auto-forwarded model: String! # same field on Vehicle → auto-forwarded displayLabel: String! # NOT on Vehicle → @SchemaMapping + service method } ``` === "MVC" ```java title="Generated VehicleService.java" public interface VehicleService { // returns Vehicle, not VehicleCard — no VehicleCard class is generated List searchVehicles(String query); String displayLabel(Vehicle vehicle); } ``` ```java title="Generated VehicleServiceController.java" @Controller public class VehicleServiceController { @QueryMapping public CompletableFuture> searchVehicles(@Argument String query) { return CompletableFuture.supplyAsync(() -> vehicleService.searchVehicles(query)); } // id, brand, model are auto-forwarded — Spring calls vehicle.getId() etc. directly. @SchemaMapping public CompletableFuture displayLabel(Vehicle vehicle) { return CompletableFuture.supplyAsync(() -> vehicleService.displayLabel(vehicle)); } } ``` === "WebFlux" ```java title="Generated VehicleService.java" public interface VehicleService { Flux searchVehicles(String query); Mono displayLabel(Vehicle vehicle); } ``` ```java title="Generated VehicleServiceController.java" @Controller public class VehicleServiceController { @QueryMapping public Flux searchVehicles(@Argument String query) { return vehicleService.searchVehicles(query); } @SchemaMapping public Mono displayLabel(Vehicle vehicle) { return vehicleService.displayLabel(vehicle); } } ``` --- ### Applied to a type — with `batch: true` `batch: true` swaps `@SchemaMapping` for `@BatchMapping`. The controller receives a list of parent objects and returns a map, eliminating N+1 queries when the field is requested for many parents at once. ```graphql title="Schema" type Vehicle { id: ID! brand: String! stats: VehicleStats! @glSkipOnServer(batch: true) } type VehicleStats @glSkipOnServer { totalMileage: Int! lastServiceDate: String! } ``` === "MVC" ```java title="Generated VehicleService.java" public interface VehicleService { Vehicle getVehicle(String id); Map stats(List vehicles); } ``` ```java title="Generated VehicleServiceController.java" @BatchMapping public CompletableFuture> stats(List vehicles) { return CompletableFuture.supplyAsync(() -> vehicleService.stats(vehicles)); } ``` === "WebFlux" ```java title="Generated VehicleService.java" public interface VehicleService { Mono getVehicle(String id); Flux stats(List vehicles); } ``` ```java title="Generated VehicleServiceController.java" @BatchMapping public Flux stats(List vehicles) { return vehicleService.stats(vehicles); } ``` !!! tip "Combine `mapTo` with `batch: true`" You can use both together: `@glSkipOnServer(mapTo: "Vehicle", batch: true)` generates a `@BatchMapping` and uses the `Vehicle` generated class as the value type in the map. --- ### Applied to a field When placed on a **field** of a non-root type (not on `Query`, `Mutation`, or `Subscription`), the field is excluded from the generated Java class and a `@SchemaMapping` is generated to resolve it. This is the correct way to declare computed fields or fields with arguments. ```graphql title="Schema" type Vehicle { id: ID! brand: String! model: String! # Computed field — not stored, resolved server-side displayName: String! @glSkipOnServer # Field with arguments — must be marked so @SchemaMapping is generated similarVehicles(limit: Int = 5): [Vehicle!]! @glSkipOnServer } ``` ```java title="Generated Vehicle.java" public class Vehicle { private final String id; private final String brand; private final String model; // displayName and similarVehicles are absent } ``` === "MVC" ```java title="Generated VehicleService.java" public interface VehicleService { Vehicle getVehicle(String id); String displayName(Vehicle vehicle); List similarVehicles(Vehicle vehicle, Integer limit); } ``` ```java title="Generated VehicleServiceController.java" @SchemaMapping public CompletableFuture displayName(Vehicle vehicle) { return CompletableFuture.supplyAsync(() -> vehicleService.displayName(vehicle)); } @SchemaMapping public CompletableFuture> similarVehicles( Vehicle vehicle, @Argument Integer limit) { return CompletableFuture.supplyAsync( () -> vehicleService.similarVehicles(vehicle, limit)); } ``` === "WebFlux" ```java title="Generated VehicleService.java" public interface VehicleService { Mono getVehicle(String id); Mono displayName(Vehicle vehicle); Flux similarVehicles(Vehicle vehicle, Integer limit); } ``` ```java title="Generated VehicleServiceController.java" @SchemaMapping public Mono displayName(Vehicle vehicle) { return vehicleService.displayName(vehicle); } @SchemaMapping public Flux similarVehicles(Vehicle vehicle, @Argument Integer limit) { return vehicleService.similarVehicles(vehicle, limit); } ``` --- ## @glSkipOnClient Excludes a type or field from the generated schema and from client-side code generation. When `generateSchema: true` is set, the annotated type or field is **completely absent from the schema Spring Boot serves** — no client can request it. See the [security guarantee](#glskiponclient-is-a-server-side-security-guarantee) section above for details. ### Applied to a type No class is generated. References to this type in queries are treated as opaque. ```graphql title="Schema" # The server sends PageInfo but the client never instantiates it directly type PageInfo @glSkipOnClient { hasNextPage: Boolean! endCursor: String startCursor: String } type VehicleConnection { nodes: [Vehicle!]! pageInfo: PageInfo! } ``` ```dart title="Generated VehicleConnection (Dart)" class VehicleConnection { final List nodes; // pageInfo is absent — PageInfo has no generated class } ``` ### Applied to a field The field is excluded from the generated class. Use this for server-only fields — passwords, internal IDs, audit metadata — that should never reach the client. ```graphql title="Schema" type User { id: ID! email: String! passwordHash: String! @glSkipOnClient # never sent to clients createdAt: String! @glSkipOnClient # internal audit field } ``` === "Dart" ```dart title="Generated User (Dart)" class User { final String id; final String email; // passwordHash and createdAt are absent } ``` === "Java client" ```java title="Generated User.java" public class User { private final String id; private final String email; // passwordHash and createdAt are absent } ``` === "TypeScript" ```typescript title="Generated User (TypeScript)" export interface User { readonly id: string; readonly email: string; // passwordHash and createdAt are absent } ``` ### Interaction with `@glMapsTo` When a field is `@glSkipOnClient`, it is also excluded from any `@glMapsTo` mapping methods. GraphLink will not reference it in `toXxx()` or `fromXxx()` constructors, preventing broken type references in generated client code. ```graphql title="Schema" input UpdateUserInput @glMapsTo(type: "UpdateUserCommand") { id: ID! email: String! internalFlag: Boolean! @glSkipOnClient # excluded from client mapping } ``` --- ## Combining both directives on different fields `@glSkipOnServer` and `@glSkipOnClient` are most often paired on **different fields of the same type** — not on the same whole type. This is the standard pattern for modelling a relationship where the server stores a raw foreign key but the client needs the resolved object. ```graphql title="Schema" type Agency { id: ID! name: String! # Server stores this ID in the database — the client never needs it cashierId: ID! @glSkipOnClient # Server resolves this via @BatchMapping — the client fetches it cashier: Cashier @glSkipOnServer(batch: true) } ``` - `cashierId` is `@glSkipOnClient`: the raw foreign key used for persistence. Invisible to the client — not generated in Dart/Java/TypeScript, and protected server-side by the access-denied `@SchemaMapping`. - `cashier` is `@glSkipOnServer(batch: true)`: a resolved object the server produces via `@BatchMapping` using the stored `cashierId`. Not stored in the database. Generated output: ```java title="Generated Agency.java (server)" public class Agency { private String id; private String name; private String cashierId; // present on server — used for DB lookups // cashier is absent — resolved via @BatchMapping } ``` ```java title="Generated AgencyServiceController.java" @BatchMapping(typeName="Agency", field="cashier") public CompletableFuture> cashier(List agencies) { return CompletableFuture.supplyAsync(() -> agencyService.cashier(agencies)); } // cashierId: access-denied @SchemaMapping generated (or stripped from schema if generateSchema: true) ``` ```dart title="Generated Agency (Dart client)" class Agency { final String id; final String name; final Cashier cashier; // present on client — typed object // cashierId is absent — @glSkipOnClient } ``` This pattern scales naturally for any number of relationships: ```graphql title="Real-world example — lot movement tracking" type LotMovement { id: ID! quantity: Int! lotId: ID! @glSkipOnClient lot: Lot @glSkipOnServer(batch: true) fromLocationId: ID @glSkipOnClient fromLocation: Location @glSkipOnServer(batch: true) toLocationId: ID @glSkipOnClient toLocation: Location @glSkipOnServer(batch: true) } ``` The client sees `lot`, `fromLocation`, and `toLocation` as fully typed objects. The server stores and works with the raw IDs, resolving the objects in batch to avoid N+1 queries. Neither side is aware of the other's representation — the schema is the only contract between them. --- # Configuration Reference Full reference for every option in `glink.json` / `glink.yaml`. ## Top-level options === "JSON" ```json title="glink.json — top-level structure" { "schemaPaths": ["schema/*.graphql"], "mode": "client", "typeMappings": { "ID": "String", "Float": "double" }, "unknownScalarType": "String", "outputDir": "lib/generated", "identityFields": ["id"], "disableCache": false, "clientConfig": { }, "serverConfig": { } } ``` === "YAML" ```yaml title="glink.yaml — top-level structure" schemaPaths: - schema/*.graphql mode: client typeMappings: ID: String Float: double unknownScalarType: String outputDir: lib/generated identityFields: - id disableCache: false clientConfig: # see sections below serverConfig: # see sections below ``` | Option | Type | Default | Description | |---|---|---|---| | `schemaPaths` | `string[]` | — | **Required.** Glob patterns for schema files. Multiple patterns are supported — all matched files are merged into one schema. | | `mode` | `"client"` \| `"server"` | `"server"` | Controls which generator runs. `"client"` generates a typed client (Dart, Java, or TypeScript). `"server"` generates Spring Boot scaffolding. | | `typeMappings` | `Record` | `{}` | Maps GraphQL scalar names to target-language types. Entries here override the defaults. Add entries for custom scalars. | | `unknownScalarType` | `string` \| `null` | `null` | Fallback target-language type for any custom scalar **not** covered by `typeMappings` or an `@glExternal` directive. Without it, an unmapped `scalar UserId` is emitted verbatim as `UserId`; set e.g. `"String"` (Dart), `"string"` (TypeScript), or `"Object"` (Java) to map all such scalars uniformly. Since one config targets a single language, write the value in that language's type syntax. `typeMappings` and `@glExternal` always take precedence. | | `outputDir` | `string` | `"src/main/java"` | Directory where generated files are written. Existing files are overwritten on every run. | | `identityFields` | `string[]` | `[]` | Field names treated as identity fields when generating `==` / `hashCode` methods (Dart and Java). Fields listed here are always included in equality regardless of `@glEqualsHashcode`. | | `disableCache` | `boolean` | `false` | When `true`, strips all `@glCache` and `@glCacheInvalidate` directives from the generated client — useful for debugging or environments where caching is handled externally. | --- ## `clientConfig.dart` Used when `mode` is `"client"` and you want Dart/Flutter output. === "JSON" ```json title="glink.json — clientConfig.dart" { "clientConfig": { "dart": { "packageName": "my_app", "httpAdapter": "http", "generateAdapters": true, "generateAllFieldsFragments": true, "autoGenerateQueries": true, "autoGenerateQueriesDefaultAlias": null, "autoGenerateQueriesFor": null, "autoGenerateQueriesArgumentLimit": 200, "maxFragmentBodySize": 8192, "defaultAlias": null, "operationNameAsParameter": false, "nullableFieldsRequired": false, "immutableInputFields": true, "immutableTypeFields": true, "captureErrors": false, "flutter": { "generateTypes": true, "generateInputs": false, "typesToSkip": [], "inputsToSkip": [], "defaultGap": 16, "booleanWidget": "switchWidget", "nullableBooleanWidget": "checkbox", "listWidget": "chips", "defaultLabelPosition": "floatingLabel", "defaultLabelWidth": 120, "defaultFormLayout": "column", "defaultRequiredIndicator": "asterisk", "defaultDebounceDuration": 300, "defaultStepperOrientation": "vertical", "defaultTypeLayout": "labeledRow", "defaultGroupLayout": "labeledRow", "defaultDatePattern": "yyyy-MM-dd", "defaultDateFirstYear": 1900, "defaultDateLastYear": 2100, "defaultDateMode": "dialog", "sdkVersion": "3.47.5", "designLibrary": "flutter" } } } } ``` === "YAML" ```yaml title="glink.yaml — clientConfig.dart" clientConfig: dart: packageName: my_app httpAdapter: http generateAdapters: true generateAllFieldsFragments: true autoGenerateQueries: true autoGenerateQueriesDefaultAlias: null autoGenerateQueriesFor: null autoGenerateQueriesArgumentLimit: 200 maxFragmentBodySize: 8192 defaultAlias: null operationNameAsParameter: false nullableFieldsRequired: false immutableInputFields: true immutableTypeFields: true captureErrors: false flutter: generateTypes: true generateInputs: false typesToSkip: [] inputsToSkip: [] defaultGap: 16 booleanWidget: switchWidget nullableBooleanWidget: checkbox listWidget: chips defaultLabelPosition: floatingLabel defaultLabelWidth: 120 defaultFormLayout: column defaultRequiredIndicator: asterisk defaultDebounceDuration: 300 defaultStepperOrientation: vertical defaultTypeLayout: labeledRow defaultGroupLayout: labeledRow defaultDatePattern: "yyyy-MM-dd" defaultDateFirstYear: 1900 defaultDateLastYear: 2100 defaultDateMode: dialog sdkVersion: "3.47.5" designLibrary: flutter ``` | Option | Type | Default | Description | |---|---|---|---| | `packageName` | `string` | — | **Required.** The Dart package name used for import paths in generated files (e.g. `"my_app"` produces `import 'package:my_app/...'`). | | `httpAdapter` | `"http"` \| `"dio"` \| `"none"` | `"http"` | Which HTTP adapter file to generate. `"http"` uses `package:http`. `"dio"` uses Dio (supports interceptors, custom `BaseOptions`). `"none"` skips adapter generation — supply your own `Future Function(String)`. | | `generateAdapters` | `boolean` | `true` | When `false`, skips generating the HTTP and WebSocket adapter files. Use this if you maintain your own adapter implementations and don't want them overwritten. | | `generateAllFieldsFragments` | `boolean` | `true` | Generates a `_all_fields_TypeName` named fragment for every type in the schema. Required for `autoGenerateQueries` to work. | | `autoGenerateQueries` | `boolean` | `true` | Automatically builds query strings for every operation using `_all_fields` fragments. When `true`, you do not need to write any query strings by hand. | | `autoGenerateQueriesDefaultAlias` | `string` \| `null` | `null` | Default alias prefix to use for auto-generated query fields when `autoGenerateQueries` is `true`. Useful when multiple operations select the same root field. | | `autoGenerateQueriesFor` | `object` \| `null` | `null` | Generates client methods only for the listed operations instead of every root field. Use instead of (or alongside) `autoGenerateQueries` when you only need a subset — e.g. on large schemas. Each key (`queries`, `mutations`, `subscriptions`) takes a list of root field names. Any listed name not found in the schema causes a build error. Requires `generateAllFieldsFragments: true`. See [Selective auto-generation](custom-queries.md#selective-auto-generation-autogeneratequeriesfor). | | `autoGenerateQueriesArgumentLimit` | `integer` \| `null` | `200` | Maximum number of propagated (nested-field) arguments allowed in the synthesized `fieldArgs` object of an auto-generated operation. When the limit is exceeded, GraphLink skips the operation and prints a `⚠` warning. Hand-written operations are never affected. Raise the value for unusually large schemas, or write a custom query with a narrower field selection instead. See [Argument limit](custom-queries.md#auto-generated-query-argument-limit). | | `maxFragmentBodySize` | `integer` \| `null` | `8192` | Maximum number of characters allowed in the serialized body of a generated `_all_fields` fragment. Fragments whose serialized body exceeds this limit are silently omitted, and any auto-generated queries that depend on them are also skipped. Set to `null` to disable the cap and always generate all fragments. Useful when `generateAllFieldsFragments: true` is used on a very large schema (e.g. Shopify's) where individual fragment bodies can reach 60 KB+, inflating both the compiled binary and every network request. | | `defaultAlias` | `string` \| `null` | `null` | Default alias applied to all generated query fields globally when no field-level alias is declared. | | `operationNameAsParameter` | `boolean` | `false` | When `true`, the generated HTTP adapter function signature includes a second `operationName` parameter: `Future Function(String payload, String operationName)`. The operation name is appended to the URL as a query parameter, which is useful for server-side logging and APM tracing. | | `nullableFieldsRequired` | `boolean` | `false` | When `true`, nullable fields in generated constructors are emitted as `required this.fieldName` instead of optional `this.fieldName`. Use this to enforce that all fields are always explicitly passed at construction sites. | | `immutableInputFields` | `boolean` | `true` | Input class fields are `final`. Recommended. | | `immutableTypeFields` | `boolean` | `true` | Response type class fields are `final`. Recommended for client-side code. | | `captureErrors` | `boolean` | `false` | When `true`, applies `@glCaptureErrors` behaviour to every query and mutation. Each method returns a `{OperationName}FullResponse` holding nullable `data` and nullable `errors` instead of throwing on GraphQL errors. Equivalent to annotating every query and mutation field with `@glCaptureErrors` in the schema. | | `appLocalizationsImport` | `string` \| `null` | `null` | Import path for an `AppLocalizations` class injected into generated Flutter UI widgets. Only relevant when `flutter` is configured. | | `flutter` | `object` | — | Enables Flutter widget generation and sets project-wide UI defaults. Omit the key entirely to disable Flutter generation. See [`clientConfig.dart.flutter`](#clientconfigdartflutter) for the full sub-key reference. | --- ## `clientConfig.dart.flutter` Flutter widget generation is enabled by adding a `flutter:` block inside `clientConfig.dart`. Omitting the block entirely disables Flutter generation. All keys are optional — unset keys use the built-in default. Keys split into two groups: **generation toggles** (what to generate) and **UI defaults** (what values are baked into generated widget constructors). ### Generation toggles | Option | Type | Default | Description | |---|---|---|---| | `generateTypes` | `boolean` | `true` | Generate stateless display widgets for schema `type` definitions (e.g. `VehicleWidget`). | | `generateInputs` | `boolean` | `false` | Generate stateful form widgets for schema `input` definitions (e.g. `AddVehicleInputForm`). | | `typesToSkip` | `string[]` | `[]` | Type names excluded from display widget generation. | | `inputsToSkip` | `string[]` | `[]` | Input names excluded from form widget generation. | ### Global widget style | Option | Type | Default | Description | |---|---|---|---| | `defaultGap` | `number` | `16` | Default spacing (dp) between rows in every generated widget and form. | | `booleanWidget` | `"switchWidget"` \| `"checkbox"` \| `"tristate"` | `"switchWidget"` | Default widget for non-nullable `Boolean` fields. `switchWidget` → `Switch`/`SwitchListTile`. `checkbox` → `Checkbox`. `tristate` → Yes/No/— dropdown. | | `nullableBooleanWidget` | `"checkbox"` \| `"tristate"` | `"checkbox"` | Default widget for nullable `Boolean?` fields. `switchWidget` is excluded — `Switch` cannot represent `null`. | | `listWidget` | `"chips"` \| `"checkboxes"` | `"chips"` | Default widget for enum-list and scalar-list fields. | ### Input form defaults These values are emitted as the default constructor parameter values in every generated form widget. They can be overridden per widget call site at runtime. | Option | Type | Default | Description | |---|---|---|---| | `defaultLabelPosition` | `"floatingLabel"` \| `"beside"` \| `"above"` | `"floatingLabel"` | Default label position across all generated forms. | | `defaultLabelWidth` | `number` | `120` | Default width (dp) of the label column in `beside` mode. | | `defaultFormLayout` | `"column"` \| `"twoColumn"` | `"column"` | Default layout for all generated forms. | | `defaultRequiredIndicator` | `"asterisk"` \| `"none"` \| `"requiredText"` \| `"optionalText"` | `"asterisk"` | Default required-field indicator style. | | `defaultDebounceDuration` | `number` | `300` | Milliseconds to wait after the last field change before firing `onChange`. | | `defaultStepperOrientation` | `"vertical"` \| `"horizontal"` | `"vertical"` | Default Flutter `StepperType` for forms that use the stepper layout (inputs with nested input fields). | ### Type display widget defaults | Option | Type | Default | Description | |---|---|---|---| | `defaultTypeLayout` | `"labeledRow"` \| `"listTile"` \| `"listTileReversed"` \| `"expandable"` | `"labeledRow"` | Default layout for all generated display widgets. | | `defaultGroupLayout` | `"labeledRow"` \| `"listTile"` \| `"listTileReversed"` | `"labeledRow"` | Default layout of the scalar accordion group inside the `expandable` layout. | ### Date picker defaults These values are baked into the generated `DateInputConfig` class defaults and into every generated date-picker helper. Per-field `DateInputConfig` values always take precedence. | Option | Type | Default | Description | |---|---|---|---| | `defaultDatePattern` | `string` | `"yyyy-MM-dd"` | Default `pattern` in the generated `DateInputConfig` class. Use `"dd/MM/yyyy"`, `"MM/dd/yyyy"`, etc. to match your locale. | | `defaultDateFirstYear` | `number` | `1900` | Fallback `firstDate` year used by all generated date pickers when no `firstDate` is provided in `DateInputConfig`. | | `defaultDateLastYear` | `number` | `2100` | Fallback `lastDate` year used by all generated date pickers when no `lastDate` is provided in `DateInputConfig`. | | `defaultDateMode` | `"dialog"` \| `"inline"` | `"dialog"` | Default `DateInputMode` in the generated `DateInputConfig` class. | | `defaultClearButton` | `boolean` | `false` | Show a clear (X) button on generated text fields. Override per field with `TextFieldOptions(clearButton: true/false)` in the form's `TextConfig`. Hidden while the field is empty or disabled, replaced by the spinner during async validation, and never shown on password fields. The tooltip comes from `FormStrings.clear`. | | `sdkVersion` | `string` | _(unset)_ | Flutter version the generated widgets target, e.g. `"3.47.5"`. From `3.35` on, generated forms use `RadioGroup` instead of the deprecated `RadioListTile.groupValue`/`onChanged`, and `DropdownButtonFormField(initialValue:)` instead of `value:`. When unset, the pre-3.35 API is generated, which works on any SDK but reports deprecation warnings on newer ones. **Quote the value** in YAML: an unquoted `3.10` is read as the number `3.1`. | | `designLibrary` | `"flutter"` \| `"standalone"` | `"flutter"` | Which Material/Cupertino library generated files import. `"flutter"` uses `package:flutter/material.dart` and `cupertino.dart`; `"standalone"` uses `package:material_ui/material_ui.dart` and `package:cupertino_ui/cupertino_ui.dart` (Flutter 3.47+, add both packages to your `pubspec.yaml`). It must match the library your app uses, because generated types such as `TextFieldOptions` expose Material types. | --- ## `clientConfig.java` Used when `mode` is `"client"` and you want a Java client. === "JSON" ```json title="glink.json — clientConfig.java" { "clientConfig": { "java": { "packageName": "com.example.generated", "wsAdapter": "java11", "jsonCodec": "jackson", "generateAllFieldsFragments": true, "autoGenerateQueries": true, "autoGenerateQueriesFor": null, "autoGenerateQueriesArgumentLimit": 200, "maxFragmentBodySize": 8192, "defaultAlias": null, "operationNameAsParameter": false, "nullableFieldsRequired": false, "immutableInputFields": true, "immutableTypeFields": true, "inputAsRecord": false, "typeAsRecord": false, "jspecify": false, "asyncStyle": "blocking", "reactiveHttpClient": "jdk", "captureErrors": false } } } ``` === "YAML" ```yaml title="glink.yaml — clientConfig.java" clientConfig: java: packageName: com.example.generated wsAdapter: java11 jsonCodec: jackson generateAllFieldsFragments: true autoGenerateQueries: true autoGenerateQueriesFor: null autoGenerateQueriesArgumentLimit: 200 maxFragmentBodySize: 8192 defaultAlias: null operationNameAsParameter: false nullableFieldsRequired: false immutableInputFields: true immutableTypeFields: true inputAsRecord: false typeAsRecord: false jspecify: false asyncStyle: blocking reactiveHttpClient: jdk captureErrors: false ``` | Option | Type | Default | Description | |---|---|---|---| | `packageName` | `string` | — | **Required.** Java package name for generated files (e.g. `"com.example.generated"`). | | `wsAdapter` | `"java11"` \| `"okhttp"` \| `"none"` | `"java11"` | Which WebSocket adapter to generate. `"java11"` uses `java.net.http.WebSocket` (zero extra dependencies). `"okhttp"` uses OkHttp's WebSocket client. `"none"` skips WebSocket adapter generation. | | `jsonCodec` | `"jackson"` \| `"gson"` \| `"none"` | `"jackson"` | Which JSON codec helper to generate. `"jackson"` generates `JacksonGraphLinkJsonCodec`. `"gson"` generates `GsonGraphLinkJsonCodec`. `"none"` skips codec generation — supply your own lambdas. | | `generateAllFieldsFragments` | `boolean` | `true` | Generates `_all_fields_TypeName` fragments for every type. Required for `autoGenerateQueries` and `autoGenerateQueriesFor`. | | `autoGenerateQueries` | `boolean` | `true` | Automatically builds query strings for every operation. | | `autoGenerateQueriesFor` | `object` \| `null` | `null` | Generates client methods only for the listed operations. Keys: `queries`, `mutations`, `subscriptions` — each takes a list of root field names. Unknown names cause a build error. Requires `generateAllFieldsFragments: true`. See [Selective auto-generation](custom-queries.md#selective-auto-generation-autogeneratequeriesfor). | | `autoGenerateQueriesArgumentLimit` | `integer` \| `null` | `200` | Maximum number of propagated (nested-field) arguments allowed in the synthesized `fieldArgs` object of an auto-generated operation. When exceeded, GraphLink skips the operation and prints a `⚠` warning. Hand-written operations are never affected. See [Argument limit](custom-queries.md#auto-generated-query-argument-limit). | | `maxFragmentBodySize` | `integer` \| `null` | `8192` | Maximum number of characters allowed in the serialized body of a generated `_all_fields` fragment. Fragments whose body exceeds this limit are silently omitted, and any auto-generated queries that depend on them are also skipped. Set to `null` to disable the cap. | | `defaultAlias` | `string` \| `null` | `null` | Default alias applied to all generated query fields globally. | | `operationNameAsParameter` | `boolean` | `false` | When `true`, the adapter interface includes an `operationName` parameter and the generated client appends it to the request URL as a query parameter. | | `nullableFieldsRequired` | `boolean` | `false` | When `true`, the generated input constructors do not accept `null` for nullable fields — `Objects.requireNonNull` is emitted for all fields. | | `immutableInputFields` | `boolean` | `true` | Input class fields are `private final`. Inputs use builder pattern. | | `immutableTypeFields` | `boolean` | `true` | Response type fields are `private final` with only getters. Recommended for client-side. | | `inputAsRecord` | `boolean` | `false` | Generates input types as Java records instead of classes with builders. Records use component accessor syntax (`field()`) instead of getters (`getField()`). Requires Java 16+. | | `typeAsRecord` | `boolean` | `false` | Generates response types as Java records. Affects how `fromJson` and mapping methods access fields. Requires Java 16+. | | `jspecify` | `boolean` | `false` | Adds `@NonNull` / `@Nullable` from `org.jspecify.annotations` to all generated type, input, and interface fields. Imports are only emitted when enabled. | | `asyncStyle` | `"blocking"` \| `"reactor"` \| `"rxjava3"` \| `"mutiny"` | `"blocking"` | Wraps query/mutation results in the library's deferred-single type (`Mono`/`Single`/`Uni`) and subscriptions in the deferred-many type (`Flux`/`Observable`/`Multi`). `"blocking"` keeps the pre-v5.0.0 direct-return / listener-callback client. See [Reactive client](java-client.md#reactive-client-v500). | | `reactiveHttpClient` | `"jdk"` \| `"webclient"` | `"jdk"` | Transport used by the default adapter under a non-blocking `asyncStyle`. `"jdk"` uses `HttpClient.sendAsync` (works with every `asyncStyle`). `"webclient"` uses Spring `WebClient` (`asyncStyle: "reactor"` only). | | `captureErrors` | `boolean` | `false` | When `true`, applies `@glCaptureErrors` behaviour to every query and mutation. Each method returns a `{OperationName}FullResponse` holding nullable `data` and nullable `errors` instead of throwing on GraphQL errors. Equivalent to annotating every query and mutation field with `@glCaptureErrors` in the schema. | --- ## `clientConfig.typescript` Used when `mode` is `"client"` and you want TypeScript output. === "JSON" ```json title="glink.json — clientConfig.typescript" { "clientConfig": { "typescript": { "httpAdapter": "fetch", "generateDefaultWsAdapter": true, "observables": false, "generateAllFieldsFragments": true, "autoGenerateQueries": true, "autoGenerateQueriesFor": null, "autoGenerateQueriesArgumentLimit": 200, "maxFragmentBodySize": 8192, "defaultAlias": null, "operationNameAsParameter": false, "optionalNullableInputFields": true, "immutableTypeFields": true, "captureErrors": false } } } ``` === "YAML" ```yaml title="glink.yaml — clientConfig.typescript" clientConfig: typescript: httpAdapter: fetch generateDefaultWsAdapter: true observables: false generateAllFieldsFragments: true autoGenerateQueries: true autoGenerateQueriesFor: null autoGenerateQueriesArgumentLimit: 200 maxFragmentBodySize: 8192 defaultAlias: null operationNameAsParameter: false optionalNullableInputFields: true immutableTypeFields: true captureErrors: false ``` | Option | Type | Default | Description | |---|---|---|---| | `httpAdapter` | `"fetch"` \| `"axios"` \| `"none"` | `"fetch"` | Which HTTP adapter to generate. `"fetch"` uses the native Fetch API (works in Angular, React, Vue, Svelte, Node 18+). `"axios"` uses Axios. `"none"` skips adapter generation. | | `generateDefaultWsAdapter` | `boolean` | `true` | When `true`, generates `DefaultGraphLinkWebSocketAdapter` implementing the graphql-ws protocol with exponential-backoff reconnect. Set to `false` if you have no subscriptions or supply your own adapter. | | `observables` | `boolean` | `false` | When `true`, query and mutation methods return `Observable` (RxJS) instead of `Promise`. Subscription methods also return `Observable`. Recommended for Angular projects. | | `generateAllFieldsFragments` | `boolean` | `true` | Generates `_all_fields_TypeName` fragments for every type. Required for `autoGenerateQueries` and `autoGenerateQueriesFor`. | | `autoGenerateQueries` | `boolean` | `true` | Automatically builds query strings for every operation using `_all_fields` fragments. | | `autoGenerateQueriesFor` | `object` \| `null` | `null` | Generates client methods only for the listed operations. Keys: `queries`, `mutations`, `subscriptions` — each takes a list of root field names. Unknown names cause a build error. Requires `generateAllFieldsFragments: true`. See [Selective auto-generation](custom-queries.md#selective-auto-generation-autogeneratequeriesfor). | | `autoGenerateQueriesArgumentLimit` | `integer` \| `null` | `200` | Maximum number of propagated (nested-field) arguments allowed in the synthesized `fieldArgs` object of an auto-generated operation. When exceeded, GraphLink skips the operation and prints a `⚠` warning. Hand-written operations are never affected. See [Argument limit](custom-queries.md#auto-generated-query-argument-limit). | | `maxFragmentBodySize` | `integer` \| `null` | `8192` | Maximum number of characters allowed in the serialized body of a generated `_all_fields` fragment. Fragments whose body exceeds this limit are silently omitted, and any auto-generated queries that depend on them are also skipped. Set to `null` to disable the cap. | | `defaultAlias` | `string` \| `null` | `null` | Default alias applied to all generated query fields globally. | | `operationNameAsParameter` | `boolean` | `false` | When `true`, the adapter function signature includes an `operationName` parameter and the client appends it to the request URL. | | `optionalNullableInputFields` | `boolean` | `true` | When `true`, nullable fields in generated input interfaces use `field?: T \| null` (TypeScript optional property). When `false`, they use `field: T \| null` (required but nullable). | | `immutableTypeFields` | `boolean` | `true` | When `true`, generated type interfaces use `readonly` on all fields. | | `captureErrors` | `boolean` | `false` | When `true`, applies `@glCaptureErrors` behaviour to every query and mutation. Each method returns a `{OperationName}FullResponse` holding optional `data` and optional `errors` instead of throwing on GraphQL errors. Equivalent to annotating every query and mutation field with `@glCaptureErrors` in the schema. | --- ## `clientConfig.kotlin` Used when `mode` is `"client"` and you want Kotlin output. === "JSON" ```json title="glink.json — clientConfig.kotlin" { "clientConfig": { "kotlin": { "packageName": "com.example.generated", "generateAllFieldsFragments": true, "autoGenerateQueries": true, "autoGenerateQueriesFor": null, "autoGenerateQueriesArgumentLimit": 200, "maxFragmentBodySize": 8192, "typeAsDataClass": true, "inputAsDataClass": true, "nullableFieldsRequired": false, "immutableTypeFields": true, "operationNameAsParameter": false, "captureErrors": false, "wsAdapter": "okhttp", "defaultAlias": null } } } ``` === "YAML" ```yaml title="glink.yaml — clientConfig.kotlin" clientConfig: kotlin: packageName: com.example.generated generateAllFieldsFragments: true autoGenerateQueries: true autoGenerateQueriesFor: null autoGenerateQueriesArgumentLimit: 200 maxFragmentBodySize: 8192 typeAsDataClass: true inputAsDataClass: true nullableFieldsRequired: false immutableTypeFields: true operationNameAsParameter: false captureErrors: false wsAdapter: okhttp defaultAlias: null ``` | Option | Type | Default | Description | |---|---|---|---| | `packageName` | `string` | — | **Required.** Kotlin package name for all generated files. | | `generateAllFieldsFragments` | `boolean` | `true` | Generates `_all_fields_TypeName` fragments for every type. Required for `autoGenerateQueries` and `autoGenerateQueriesFor`. | | `autoGenerateQueries` | `boolean` | `true` | Automatically builds query strings for every operation using `_all_fields` fragments. | | `autoGenerateQueriesFor` | `object` \| `null` | `null` | Generates client methods only for the listed operations. Keys: `queries`, `mutations`, `subscriptions` — each takes a list of root field names. Unknown names cause a build error. Requires `generateAllFieldsFragments: true`. See [Selective auto-generation](custom-queries.md#selective-auto-generation-autogeneratequeriesfor). | | `autoGenerateQueriesArgumentLimit` | `integer` \| `null` | `200` | Maximum number of propagated (nested-field) arguments allowed in the synthesized `fieldArgs` object of an auto-generated operation. When exceeded, GraphLink skips the operation and prints a `⚠` warning. Hand-written operations are never affected. See [Argument limit](custom-queries.md#auto-generated-query-argument-limit). | | `maxFragmentBodySize` | `integer` \| `null` | `8192` | Maximum number of characters allowed in the serialized body of a generated `_all_fields` fragment. Fragments whose body exceeds this limit are silently omitted, and any auto-generated queries that depend on them are also skipped. Set to `null` to disable the cap. | | `typeAsDataClass` | `boolean` | `true` | Emit output types as `data class`. When `false`, uses `open class`. | | `inputAsDataClass` | `boolean` | `true` | Emit input types as `data class`. When `false`, uses `open class`. | | `nullableFieldsRequired` | `boolean` | `false` | When `true`, nullable constructor parameters have no default — callers must pass them explicitly. | | `immutableTypeFields` | `boolean` | `true` | Generate `val` (immutable) fields instead of `var`. | | `operationNameAsParameter` | `boolean` | `false` | When `true`, appends the operation name as a `?operationName=` query parameter in the HTTP request URL. | | `captureErrors` | `boolean` | `false` | When `true`, applies `@glCaptureErrors` to every query and mutation. Each method returns a `{OperationName}FullResponse` with `data` and `errors` instead of throwing. | | `wsAdapter` | `"okhttp"` \| `"none"` | `"okhttp"` | WebSocket adapter to generate. `"okhttp"` emits `DefaultGraphLinkWebSocketAdapter` using OkHttp; `"none"` emits only the `GraphLinkWebSocketAdapter` interface. | | `defaultAlias` | `string?` | `null` | Default alias applied to all generated query fields globally. | --- ## `clientConfig.swift` Used when `mode` is `"client"` and you want Swift output. See the [Swift Client](swift-client.md) page for a full walkthrough. === "JSON" ```json title="glink.json — clientConfig.swift" { "clientConfig": { "swift": { "moduleName": "GraphLinkGenerated", "generateAllFieldsFragments": true, "autoGenerateQueries": true, "autoGenerateQueriesFor": null, "autoGenerateQueriesArgumentLimit": 200, "maxFragmentBodySize": 8192, "nullableFieldsRequired": false, "immutableTypeFields": true, "operationNameAsParameter": false, "captureErrors": false, "wsAdapter": "urlsession", "defaultAlias": null } } } ``` === "YAML" ```yaml title="glink.yaml — clientConfig.swift" clientConfig: swift: moduleName: GraphLinkGenerated generateAllFieldsFragments: true autoGenerateQueries: true autoGenerateQueriesFor: null autoGenerateQueriesArgumentLimit: 200 maxFragmentBodySize: 8192 nullableFieldsRequired: false immutableTypeFields: true operationNameAsParameter: false captureErrors: false wsAdapter: urlsession defaultAlias: null ``` | Option | Type | Default | Description | |---|---|---|---| | `moduleName` | `string` | — | **Required.** Used in doc comments / generated `Package.swift` (if emitted) — generated files have no module declaration of their own. | | `generateAllFieldsFragments` | `boolean` | `true` | Generates `_all_fields_TypeName` fragments for every type. Required for `autoGenerateQueries` and `autoGenerateQueriesFor`. | | `autoGenerateQueries` | `boolean` | `true` | Automatically builds query strings for every operation using `_all_fields` fragments. | | `autoGenerateQueriesFor` | `object` \| `null` | `null` | Generates client methods only for the listed operations. Keys: `queries`, `mutations`, `subscriptions` — each takes a list of root field names. Unknown names cause a build error. Requires `generateAllFieldsFragments: true`. See [Selective auto-generation](custom-queries.md#selective-auto-generation-autogeneratequeriesfor). | | `autoGenerateQueriesArgumentLimit` | `integer` \| `null` | `200` | Maximum number of propagated (nested-field) arguments allowed in the synthesized `fieldArgs` object of an auto-generated operation. When exceeded, GraphLink skips the operation and prints a `⚠` warning. Hand-written operations are never affected. See [Argument limit](custom-queries.md#auto-generated-query-argument-limit). | | `maxFragmentBodySize` | `integer` \| `null` | `8192` | Maximum number of characters allowed in the serialized body of a generated `_all_fields` fragment. Fragments whose body exceeds this limit are silently omitted, and any auto-generated queries that depend on them are also skipped. Set to `null` to disable the cap. | | `nullableFieldsRequired` | `boolean` | `false` | When `true`, nullable initializer parameters have no default — callers must pass them explicitly. | | `immutableTypeFields` | `boolean` | `true` | Generate `let` (immutable) fields instead of `var`. | | `operationNameAsParameter` | `boolean` | `false` | When `true`, appends the operation name as a `?operationName=` query parameter in the HTTP request URL. | | `captureErrors` | `boolean` | `false` | When `true`, applies `@glCaptureErrors` to every query and mutation. Each method returns a `{OperationName}FullResponse` with optional `data` and optional `errors` instead of throwing. | | `wsAdapter` | `"urlsession"` \| `"none"` | `"urlsession"` | WebSocket adapter to generate. `"urlsession"` emits `DefaultGraphLinkURLSessionWebSocketAdapter` built on `URLSessionWebSocketTask` (Foundation, no extra dependency); `"none"` emits only the `GraphLinkClientAdapter`/WebSocket typealiases. There is no HTTP-adapter enum for Swift — `URLSession` is part of Foundation on every Apple platform plus Linux, so there's nothing to choose between. | | `defaultAlias` | `string?` | `null` | Default alias applied to all generated query fields globally. | --- ## `serverConfig.spring` Used when `mode` is `"server"`. === "JSON" ```json title="glink.json — serverConfig.spring" { "serverConfig": { "spring": { "basePackage": "com.example.generated", "generateControllers": true, "generateRepositories": false, "immutableInputFields": true, "immutableTypeFields": false, "inputAsRecord": false, "typeAsRecord": false, "reactive": false, "useSpringSecurity": false, "injectDataFetching": false, "generateSchema": false, "schemaTargetPath": null } } } ``` === "YAML" ```yaml title="glink.yaml — serverConfig.spring" serverConfig: spring: basePackage: com.example.generated generateControllers: true generateRepositories: false immutableInputFields: true immutableTypeFields: false inputAsRecord: false typeAsRecord: false reactive: false useSpringSecurity: false injectDataFetching: false generateSchema: false schemaTargetPath: null ``` | Option | Type | Default | Description | |---|---|---|---| | `basePackage` | `string` | — | **Required.** Java package name for all generated classes (e.g. `"com.example.generated"`). | | `generateControllers` | `boolean` | `true` | Generates `@Controller` classes with `@QueryMapping`, `@MutationMapping`, `@SubscriptionMapping`, and `@Argument` on parameters. | | `generateRepositories` | `boolean` | `false` | Generates JPA `Repository` interfaces for types annotated with `@glRepository`. | | `immutableInputFields` | `boolean` | `true` | Input class fields are `private final`. Recommended. | | `immutableTypeFields` | `boolean` | `false` | Type class fields are `private final`. Set to `false` for Spring Boot — Spring's runtime sets fields via setters or reflection. | | `inputAsRecord` | `boolean` | `false` | Generates input types as Java records. Requires Java 16+. | | `typeAsRecord` | `boolean` | `false` | Generates type classes as Java records. Requires Java 16+. | | `reactive` | `boolean` | `false` | When `true`, generates Spring WebFlux-style controllers. Queries and mutations return `Mono`. File upload fields use `FilePart`. | | `useSpringSecurity` | `boolean` | `false` | MVC mode only. Captures the `SecurityContext` before entering `CompletableFuture` lambdas and restores it on the worker thread. Requires `spring-security` on the classpath. Has no effect in reactive mode. | | `injectDataFetching` | `boolean` | `false` | When `true`, generates data-fetching injection code in controllers for field-level resolution. | | `generateSchema` | `boolean` | `false` | When `true`, writes a copy of the processed schema to disk alongside the generated Java. Requires `schemaTargetPath`. | | `schemaTargetPath` | `string` \| `null` | `null` | Path where the schema file is written when `generateSchema` is `true`. Must end in `.graphql` or `.graphqls`. Example: `"src/main/resources/schema.graphqls"`. | !!! info "Strict by default" Since v5.0.0, generated server types enforce real schema nullability by default, and controllers serialize responses to a `Map` via `toJson()`. See [Strict server generation](spring-server.md#strict-server-generation), and the `@glServerLenient` directive in the [Directives reference](directives.md). --- ## `serverConfig.kotlinSpring` Used when `mode` is `"server"` and you want a Kotlin Spring server instead of Java. See [Kotlin Spring Boot server](spring-server.md#kotlin-spring-boot-server) for the full walkthrough. === "JSON" ```json title="glink.json — serverConfig.kotlinSpring" { "serverConfig": { "kotlinSpring": { "basePackage": "com.example.generated", "typeAsDataClass": true, "inputAsDataClass": true, "blockingServices": true, "generateControllers": true, "generateRepositories": false, "immutableInputFields": true, "immutableTypeFields": false, "generateSchema": false, "schemaTargetPath": null } } } ``` === "YAML" ```yaml title="glink.yaml — serverConfig.kotlinSpring" serverConfig: kotlinSpring: basePackage: com.example.generated typeAsDataClass: true inputAsDataClass: true blockingServices: true generateControllers: true generateRepositories: false immutableInputFields: true immutableTypeFields: false generateSchema: false schemaTargetPath: null ``` | Option | Type | Default | Description | |---|---|---|---| | `basePackage` | `string` | — | **Required.** Kotlin package name for all generated files. | | `typeAsDataClass` | `boolean` | `false` | Emit output types as `data class`. When `false`, uses `open class`. | | `inputAsDataClass` | `boolean` | `false` | Emit input types as `data class`. When `false`, uses `open class`. | | `blockingServices` | `boolean` | `true` | Wraps each generated controller's service call in `withContext(Dispatchers.IO + SecurityCoroutineContext()) { ... }`. Set `false` for a coroutine-native, non-blocking service layer. | The remaining options (`generateControllers`, `generateRepositories`, `immutableInputFields`, `immutableTypeFields`, `generateSchema`, `schemaTargetPath`) behave the same as their `serverConfig.spring` equivalents above. --- ## Identifier normalization Since v5.0.0, every generated identifier — field names, argument names, enum values, and type/input/interface/union/enum names — is unconditionally rewritten to the target language's canonical casing convention (lowerCamelCase fields for Dart/Java/Kotlin/TypeScript, SCREAMING_SNAKE enum values for Java/Kotlin, PascalCase enum values for TypeScript, PascalCase type names everywhere). This is not opt-in and there is no config flag to disable it. A GraphQL name that collides with a target-language reserved keyword (e.g. an argument or field named `default`, `return`, `object`) is also automatically sanitized (`default_`, with numeric suffixing on further collisions), while the wire/GraphQL name is preserved. This applies consistently to fields, arguments, enum values, resolver parameters, `@glMapsTo` mappings, and Spring controller/service signatures. **Migration:** if your schema's GraphQL names aren't already in canonical casing (e.g. `snake_case` fields, lowercase type names), regenerating renames the corresponding generated fields/classes/methods. Review the diff after upgrading and update any application code that references generated identifiers directly. Because the generated identifier can now differ from the wire name, `toJson`/`fromJson` are mandatory on every serializer (Dart, Java, Kotlin, TypeScript) as of v5.0.0 — code that reads/writes generated objects without going through them can no longer assume the two names match. --- ## Config file format GraphLink accepts **JSON** (`.json`), **YAML** (`.yaml`), and **YML** (`.yml`) config files. The structure and keys are identical across all three formats. ```yaml title="glink.yaml — equivalent to the JSON example above" schemaPaths: - schema/*.graphql mode: client typeMappings: ID: String Float: double outputDir: lib/generated clientConfig: dart: packageName: my_app generateAllFieldsFragments: true autoGenerateQueries: true ``` --- ## CLI flags ```bash glink # auto-discover config (see below) glink -c glink.json # explicit config path glink -w # watch mode — regenerate on schema change glink -w --debounce 1000 # watch mode, wait 1s after the last change glink --version # print version and exit glink --help # print usage ``` | Flag | Description | |---|---| | `-c ` | Path to the config file. Accepts `.json`, `.yaml`, or `.yml`. If omitted, `glink` searches for `glink.json`, `glink.yaml`, or `glink.yml` starting from the current directory and walking up to the filesystem root. | | `-w` | Watch mode. GraphLink monitors the files matched by `schemaPaths` and regenerates after changes settle (see `--debounce`). | | `--debounce ` | Watch mode only. Milliseconds to wait after the last detected schema change before regenerating, so a burst of saves triggers a single run. Default `500`. Must be a non-negative integer. | | `--version` | Prints the GraphLink version and exits. | | `--help` | Prints usage information and exits. | --- # Using GraphLink with AI Agents AI coding assistants work best when they understand how your project is structured. Without instructions, an agent seeing a `generated/` directory full of Dart or Java files will waste time reading, editing, or recreating code that GraphLink owns. This page provides ready-to-use instruction files for common setups. **Copy the one that matches your project and paste it into your agent's instruction file.** | Agent | Instruction file | |---|---| | Claude Code | `CLAUDE.md` at project root | | Gemini CLI | `GEMINI.md` or `.gemini/instructions.md` | | GitHub Copilot | `.github/copilot-instructions.md` | | Cursor | `.cursor/rules` | | Any agent | `AGENTS.md` at project root (read by most tools) | --- ## Dart / Flutter project === "AGENTS.md / CLAUDE.md" ```markdown ## Code generation — GraphLink This project uses [GraphLink](https://graphlink.dev) (`glink`) to generate Dart/Flutter code from a GraphQL schema. - **Schema**: `lib/**/*.graphql` (or wherever your .graphql files are) - **Config**: `glink.json` (or `glink.yaml`) at the project root - **Generated files**: `lib/generated/` — DO NOT edit these manually To regenerate after any schema change: ```bash glink -c glink.json # or with watch mode: glink -c glink.json -w ``` ### Rules - To add a field, type, query, or mutation: edit the `.graphql` schema file, then run `glink`. - Never edit files inside `lib/generated/` — they will be overwritten. - The generated `GraphLinkClient` is the entry point for all queries, mutations, and subscriptions. - Input classes live in `lib/generated/inputs/`. Type classes live in `lib/generated/types/`. - Enums are in `lib/generated/enums/` and are already serialized — use them directly. ### Caching Cache behaviour is declared in the schema via `@glCache` and `@glCacheInvalidate` directives. Do not implement caching in application code — add or modify the directive in the schema instead. ``` --- ## Java client project === "AGENTS.md / CLAUDE.md" ```markdown ## Code generation — GraphLink This project uses [GraphLink](https://graphlink.dev) (`glink`) to generate Java client code from a GraphQL schema. - **Schema**: `schema/*.graphql` (or wherever your .graphql files are) - **Config**: `glink.json` at the project root - **Generated files**: `src/main/java/com/example/generated/` — DO NOT edit these manually To regenerate after any schema change: ```bash glink -c glink.json ``` ### Rules - To add a field, type, query, or mutation: edit the `.graphql` schema file, then run `glink`. - Never edit files inside the `generated/` package. - `GraphLinkClient` is the entry point. Queries are on `client.queries`, mutations on `client.mutations`. - Return types are fully resolved — no `TypeReference`, no casting needed at the call site. - Input classes use a builder pattern: `AddVehicleInput.builder().brand("Toyota").build()`. ``` --- ## TypeScript project === "AGENTS.md / CLAUDE.md" ```markdown ## Code generation — GraphLink This project uses [GraphLink](https://graphlink.dev) (`glink`) to generate TypeScript client code from a GraphQL schema. - **Schema**: `schema/*.graphql` (or wherever your .graphql files are) - **Config**: `glink.json` at the project root - **Generated files**: `src/generated/` — DO NOT edit these manually To regenerate after any schema change: ```bash glink -c glink.json ``` ### Rules - To add a field, type, query, or mutation: edit the `.graphql` schema file, then run `glink`. - Never edit files inside `src/generated/`. - Import `GraphLinkClient` from `src/generated/client/graph-link-client`. - All queries and mutations are typed methods — no manual type assertions needed. - Subscriptions are on `client.subscriptions`: callback-based (`onData`/`onError`/`onComplete`) by default, or `Observable` when `observables: true` is set in the TypeScript config. ``` --- ## Kotlin client project === "AGENTS.md / CLAUDE.md" ```markdown ## Code generation — GraphLink This project uses [GraphLink](https://graphlink.dev) (`glink`) to generate Kotlin client code from a GraphQL schema. - **Schema**: `schema/*.graphql` (or wherever your .graphql files are) - **Config**: `glink.json` (or `glink.yaml`) at the project root - **Generated files**: `src/main/kotlin/com/example/generated/` — DO NOT edit these manually To regenerate after any schema change: ```bash glink -c glink.json # or with watch mode: glink -c glink.json -w ``` ### Rules - To add a field, type, query, or mutation: edit the `.graphql` schema file, then run `glink`. - Never edit files inside the `generated/` package — they will be overwritten on next run. - `GraphLinkClient` is the entry point. Queries are on `client.queries`, mutations on `client.mutations`, subscriptions on `client.subscriptions`. - All query and mutation methods are `suspend` functions — call them from a coroutine or `runBlocking`. - Subscription methods return a `Flow` — collect them inside a coroutine scope. - Generated types and inputs are `data class` by default — construct them directly, no builder needed. - Nullable input fields have `= null` defaults — only pass the fields you need. - Enums are in `generated/enums/` with `toJson()` / `fromJson()` — use them directly, do not re-serialize. ### Caching Cache behaviour is declared in the schema via `@glCache` and `@glCacheInvalidate` directives. Do not implement caching in application code — add or modify the directive in the schema instead. ``` --- ## Swift client project === "AGENTS.md / CLAUDE.md" ```markdown ## Code generation — GraphLink This project uses [GraphLink](https://graphlink.dev) (`glink`) to generate Swift client code from a GraphQL schema. - **Schema**: `schema/*.graphql` (or wherever your .graphql files are) - **Config**: `glink.json` (or `glink.yaml`) at the project root - **Generated files**: `Sources/GraphLinkGenerated/` — DO NOT edit these manually To regenerate after any schema change: ```bash glink -c glink.json # or with watch mode: glink -c glink.json -w ``` ### Rules - To add a field, type, query, or mutation: edit the `.graphql` schema file, then run `glink`. - Never edit files inside `Sources/GraphLinkGenerated/` — they will be overwritten on next run. - `GraphLinkClient` is the entry point. Queries are on `client.queries`, mutations on `client.mutations`, subscriptions on `client.subscriptions`. - All query, mutation, and subscription methods are `async throws` — call them from an `async` context. - Subscription methods return an `AsyncThrowingStream` — iterate with `for try await`. - Generated types and inputs are `Sendable` structs with a memberwise `init` — construct them directly, no builder needed. - Nullable input fields default to `nil` in `init` — only pass the fields you need. - Enums and types are in `Enums/` and `Types/` with `toJson()` / `fromJson()` — use them directly, do not re-serialize. ### Caching Cache behaviour is declared in the schema via `@glCache` and `@glCacheInvalidate` directives. Do not implement caching in application code — add or modify the directive in the schema instead. ``` --- ## Spring Boot server project === "AGENTS.md / CLAUDE.md" ```markdown ## Code generation — GraphLink This project uses [GraphLink](https://graphlink.dev) (`glink`) to generate Spring Boot server code from a GraphQL schema. - **Schema**: `src/main/resources/graphql/*.graphqls` - **Config**: `glink.json` at the project root - **Generated files**: `src/main/java/com/example/generated/` — DO NOT edit these manually To regenerate after any schema change: ```bash glink -c glink.json ``` ### Rules - To add a field, type, query, or mutation: edit the `.graphqls` schema file, then run `glink`. - Never edit files inside the `generated/` package — controllers, inputs, and types are all generated. - Implement the generated service interfaces (e.g. `VehicleService`) — do not modify the controllers. - Subscription resolvers must return `reactor.core.publisher.Flux`. - Generated repositories are stubs — fill in the persistence logic, but do not change the interface signature. ``` --- ## Express / Apollo server project === "AGENTS.md / CLAUDE.md" ```markdown ## Code generation — GraphLink This project uses [GraphLink](https://graphlink.dev) (`glink`) to generate an Express + Apollo Server scaffold (TypeScript) from a GraphQL schema. - **Schema**: `schema/*.graphql` - **Config**: `glink.json` at the project root (`mode: server`, `serverConfig.expressApollo`) - **Generated files**: `src/generated/` — DO NOT edit these manually - **Your code**: `src/impl/` (service implementations) and `src/start.ts` To regenerate after any schema change: ```bash glink -c glink.json ``` ### Rules - To add a field, type, query, or mutation: edit the `.graphql` schema file, then run `glink`. - Never edit files inside `src/generated/` — resolvers, types, inputs, and `index.ts` are all generated. - Implement the generated service interfaces (e.g. `ArticleService`) in `src/impl/` — do not modify the resolvers. - Queries and mutations return `Promise`; subscriptions return `AsyncIterable`. - Start the server with the generated `createServer({ ...services })`; per-request context goes through `contextFactory`. - Batch-mapped fields (`@glSkipOnServer(batch: true)`) use generated DataLoaders — do not hand-roll N+1 lookups. - If the schema uses `@glIntercept`, pass an `interceptor` implementation in the services object. ``` --- ## Full-stack project (Flutter + Spring Boot) === "AGENTS.md / CLAUDE.md" ```markdown ## Code generation — GraphLink This project uses [GraphLink](https://graphlink.dev) (`glink`) to generate code from a single GraphQL schema for both the Flutter client and the Spring Boot backend. ### Schema One schema file drives both ends: `schema/schema.graphql` ### Client (Flutter) - **Config**: `client/glink.json` - **Generated files**: `client/lib/generated/` — DO NOT edit ### Server (Spring Boot) - **Config**: `server/glink.json` - **Generated files**: `server/src/main/java/com/example/generated/` — DO NOT edit To regenerate both after a schema change: ```bash glink -c client/glink.json glink -c server/glink.json ``` ### Rules - All data model changes go in `schema/schema.graphql` first — then regenerate. - On the server, implement the generated service interfaces. Never edit generated controllers. - On the client, use `GraphLinkClient` — never write raw HTTP or WebSocket calls for GraphQL. - Cache behaviour is declared in the schema via `@glCache` / `@glCacheInvalidate`. Do not implement caching in application code. ``` --- !!! tip "Watch mode during development" Run `glink -c glink.json -w` in a terminal while your AI agent edits the schema. Every schema save triggers instant regeneration — the agent's next read of the generated files will already reflect the change. !!! info "The generated code is yours" GraphLink has zero runtime dependency. If you stop using it, every generated file continues to compile and work. Commit the generated files to version control — they are source of truth for what your schema produces. ---