GraphQL·C#·Free

GraphQL to
C#.

Turn a GraphQL SDL schema into a modern C# client. Records with required and init, nullable reference types straight from the schema's !, a [JsonPolymorphic] interface for every union. Typed query, mutation, and subscription clients over HttpClient — subscriptions surface as IAsyncEnumerable<T> over a graphql-transport-ws socket. No signup, no install.

SDL·C# 12·HttpClient·System.Text.Json·MIT
schema.graphqlInput
scalar DateTime

enum Currency {
  EUR
  USD
}

enum PaymentStatus {
  PENDING
  SETTLED
  FAILED
}

type CardPayment {
  last4: String!
  brand: String!
}

type BankPayment {
  iban: String!
}

union PaymentMethod = CardPayment | BankPayment

type Payment {
  id: ID!
  customerId: String!
  amount: Int!
  currency: Currency!
  status: PaymentStatus!
  description: String
  method: PaymentMethod
  createdAt: DateTime!
}
compile
Payment.cs tsc cleanOutput
// Models/Payment.cs
using System;
using System.Text.Json.Serialization;

namespace Example.Payments.Models;

public record Payment
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
    [JsonPropertyName("customerId")]
    public required string CustomerId { get; init; }
    [JsonPropertyName("amount")]
    public required int Amount { get; init; }
    [JsonPropertyName("currency")]
    public required Currency Currency { get; init; }
    [JsonPropertyName("status")]
    public required PaymentStatus Status { get; init; }
    [JsonPropertyName("description")]
    public string? Description { get; init; }
    [JsonPropertyName("method")]
    public PaymentMethod? Method { get; init; }
    [JsonPropertyName("createdAt")]
    public required DateTime CreatedAt { get; init; }
}
What gets generated

Idiomatic C#. Not a generated-sources folder.

Below: types emitted from a small payments schema with a union, a nullable field, and an enum. Every type is a record with init properties; required covers every non-null SDL field; the union becomes a [JsonPolymorphic] interface keyed on __typename, so a plain switch matches it.

// Models/Payment.cs
public record Payment
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
    [JsonPropertyName("description")]
    public string? Description { get; init; }
    [JsonPropertyName("method")]
    public PaymentMethod? Method { get; init; }
    [JsonPropertyName("createdAt")]
    public required DateTime CreatedAt { get; init; }
    // ...
}

// Models/Currency.cs
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Currency
{
    EUR,
    USD
}

// Models/PaymentMethod.cs
[JsonPolymorphic(TypeDiscriminatorPropertyName = "__typename")]
[JsonDerivedType(typeof(CardPayment), "CardPayment")]
[JsonDerivedType(typeof(BankPayment), "BankPayment")]
public interface PaymentMethod { }

// Match the union with a plain switch — System.Text.Json
// reads __typename and hands you the concrete record:
string Label(Payment p) => p.Method switch
{
    CardPayment c => $"card {c.Last4}",
    BankPayment b => $"bank {b.Iban}",
    null          => "no method",
    _             => throw new InvalidOperationException(),
};
Three things we get right

Grounded in how the schema reads.

Each claim is something you can verify by running the converter on your own .graphql schema. Click a card to see the SDL fragment that triggered it and the C# that came out.

01ProofRecords with required and init — nullability comes from the schema.Spec · InCode · Out
schema.graphql fragment
type Payment {
  id: ID!
  description: String
  method: PaymentMethod
  createdAt: DateTime!
  # ...
}
Payment.cs
public record Payment
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
    [JsonPropertyName("description")]
    public string? Description { get; init; }
    [JsonPropertyName("method")]
    public PaymentMethod? Method { get; init; }
    [JsonPropertyName("createdAt")]
    public required DateTime CreatedAt { get; init; }
    // ...
}

// - record, not a class with setters
// - 'String!' -> required string; 'String' -> string?
// - a nullable union field becomes PaymentMethod?
// - DateTime via --custom-scalar DateTime=System.DateTime
// - [JsonPropertyName] pins the camelCase wire name
C# · HttpClient

Typed clients, not a raw transport.

Operations split into QueryClient, MutationClient, and SubscriptionClient, all extending a thin BaseHttpClient that owns the HttpClient and base URL. Queries and mutations POST a GraphQL document — with the full selection set generated inline — to /graphql; ExecuteGraphQLAsync reads data.<field> and surfaces an errors[] array as an HttpRequestException. Subscriptions return IAsyncEnumerable<T> over a graphql-transport-ws WebSocket that the base client derives from the base URL. System.Text.Json handles both sides.

Client/QueryClient.cs generated
// Client/QueryClient.cs
using System.Net.Http;
using System.Text.Json;
using Example.Payments.Models;

namespace Example.Payments.Client;

public class QueryClient : BaseHttpClient
{
    public QueryClient(HttpClient httpClient, string baseUrl, JsonSerializerOptions? jsonOptions)
        : base(httpClient, baseUrl, jsonOptions) { }

    public async Task<Example.Payments.Models.Payment?> PaymentAsync(
        string id, CancellationToken cancellationToken = default)
    {
        var variables = new Dictionary<string, object?> { { "id", id } };
        return await ExecuteGraphQLAsync<Example.Payments.Models.Payment>(
            "query Payment($id: ID!) { payment(id: $id) { ... } }",  // full selection set generated inline
            "Payment", variables, "payment", cancellationToken).ConfigureAwait(false);
    }
}

// Client/SubscriptionClient.cs
public class SubscriptionClient : BaseHttpClient
{
    public async IAsyncEnumerable<Example.Payments.Models.Payment> PaymentSettledAsync(
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await foreach (var item in SubscribeAsync<Example.Payments.Models.Payment>(
            "subscription PaymentSettled { paymentSettled { ... } }",
            "PaymentSettled", null, "paymentSettled", cancellationToken).ConfigureAwait(false))
        {
            yield return item;
        }
    }
}

// Client/BaseHttpClient.cs (shared)
public abstract class BaseHttpClient
{
    protected readonly HttpClient _httpClient;
    protected readonly string _baseUrl;
    protected readonly JsonSerializerOptions? _jsonOptions;
    protected readonly GraphQLWebSocketConnection _ws;

    public BaseHttpClient(HttpClient httpClient, string baseUrl, JsonSerializerOptions? jsonOptions = null)
    {
        _httpClient = httpClient;
        _baseUrl = baseUrl.TrimEnd('/');
        _jsonOptions = jsonOptions;
        // http -> ws, https -> wss; subscriptions hit <baseUrl>/graphql
        var wsBase = _baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
            ? "wss://" + _baseUrl.Substring("https://".Length)
            : "ws://" + _baseUrl.Substring("http://".Length);
        _ws = new GraphQLWebSocketConnection(new Uri(wsBase + "/graphql"), _jsonOptions);
    }

    protected async Task<T?> ExecuteGraphQLAsync<T>(
        string query, string operationName,
        Dictionary<string, object?>? variables, string fieldName,
        CancellationToken cancellationToken)
    {
        var graphqlBody = new Dictionary<string, object?>
        {
            { "query", query },
            { "operationName", operationName },
        };
        if (variables != null) graphqlBody["variables"] = variables;
        using var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/graphql");
        request.Content = new StringContent(
            JsonSerializer.Serialize(graphqlBody, _jsonOptions), Encoding.UTF8, "application/json");
        using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
        response.EnsureSuccessStatusCode();
        // Reads data.<fieldName>; an errors[] array surfaces as HttpRequestException.
        return await ReadGraphQLFieldAsync<T>(response, fieldName, cancellationToken).ConfigureAwait(false);
    }

    // Subscriptions: one ClientWebSocket (graphql-transport-ws) multiplexes every
    // active subscription; each yields its 'next' frames as IAsyncEnumerable<T>.
    protected async IAsyncEnumerable<T> SubscribeAsync<T>(
        string query, string operationName,
        Dictionary<string, object?>? variables, string fieldName,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    { /* connection_init -> subscribe -> read next/error/complete */ }
}
Also available via MCP

Or ask Claude to do it.

The same generator ships as a Model Context Protocol server. It calls the same web API this converter uses, so the output is identical. Point Claude Desktop, Cursor, or any MCP-aware agent at a .graphql schema and it produces the C# client as a diff — free, no token, no leaving the chat.

// claude-desktop config · .mcp.json
{
  "mcpServers": {
    "metaengine": {
      "command": "npx",
      "args": ["-y", "@metaengine/mcp-server"]
    }
  }
}

// Then in Claude:
// "Load schema.graphql and generate a C# HttpClient
//  in namespace Acme.Payments."
See the MCP page
Prefer it in your build?

Same generator, as a package.

The converter above is the fastest way to try the output. For CI and repeatable builds, the identical generator ships as a package — pin a version, wire it into your pipeline, and review diffs like any other code.

Questions

Things people ask.

The questions we keep getting about GraphQL → C#. If yours is not here, run the converter — the answer is usually in the output.

A namespace of .cs files: one record per object and input type, one enum per enum, a [JsonPolymorphic] interface plus member records per union, and a typed client per operation root — QueryClient, MutationClient, SubscriptionClient. Plus a shared BaseHttpClient with the GraphQL-over-HTTP POST helper and the subscription WebSocket. System.Text.Json only — no extra runtime package.
More converters

Same engine, other stacks.

Every converter runs the same spec-to-IR pipeline. Pick another source format or target stack.

OpenAPITypeScript
Angular httpResource, React TanStack Query, or framework-agnostic fetch.
Open converter
OpenAPIAngular
Typed services with httpResource, Signals, inject() DI, and interceptors.
Open converter
OpenAPIReact
TanStack Query hooks with stable query keys, or plain async functions.
Open converter
OpenAPIFetch
Framework-agnostic fetch client with ApiResult, middleware, and retries.
Open converter
OpenAPIJava Spring
RestClient services, Java records, sealed interfaces, Spring Boot 3.
Open converter
OpenAPIPython
Async httpx clients paired with Pydantic v2 models.
Open converter
OpenAPIGo
Idiomatic net/http client, context-aware, pointer-nullable fields.
Open converter
OpenAPIKotlin
Ktor client, kotlinx.serialization, sealed interfaces for oneOf.
Open converter
OpenAPIC#
Records, nullable reference types, JsonPolymorphic, HttpClient.
Open converter
OpenAPIRust
reqwest client with serde-tagged enums for oneOf and Option<T>.
Open converter
GraphQLAngular
Observable services with typed queries, mutations, and graphql-ws subscriptions.
Open converter
GraphQLReact
TanStack Query hooks with discriminated-union types and query keys.
Open converter
GraphQLKotlin
Ktor, kotlinx.serialization, Flow-based subscriptions.
Open converter
ProtobufKotlin
Ktor, coroutines, and sealed classes straight from .proto.
Open converter
ProtobufC#
Records, nullable types, Connect RPC over HttpClient.
Open converter
SQLTypeScript
Postgres DDL to typed interfaces with foreign-key navigation.
Open converter
SQLKotlin
CREATE TABLE scripts to Kotlin @Serializable data classes.
Open converter

Try it on your own schema.

The converter runs in the browser. Your schema never leaves the page.

Open the converter