Changelog v3.20.0
Version 3.20.0 (2026-07-09)
This release introduces the Dart client code generator — a new NpgsqlRest.DartClient plugin that generates Dart fetch modules for Flutter projects, the same way NpgsqlRest.TsClient generates TypeScript modules. Also included: TanStack Query (React Query) hooks generation for the TypeScript client, function-calling schemas (OpenAI/Anthropic) and llms.txt generated from the MCP tool set, support for the HTTP QUERY method, and a configurable OpenAPI specification version (3.0 / 3.1) for the OpenAPI plugin.
1. Dart client code generation (NpgsqlRest.DartClient 1.0.0)
For every NpgsqlRest endpoint the generator emits idiomatic Dart built on package:http (no other dependencies, works on all Flutter targets — mobile, desktop, web):
- Request classes with
toJson()— one field per routine parameter, optional-named constructor parameters. Parameters with PostgreSQL defaults are omitted from the JSON body and query string when null (so the database default applies), mirroring the TypeScript client's optional-property semantics. - Response classes with a
fromJson()factory — one field per column,snake_casecolumns becomelowerCamelCasefields with the raw wire name kept as the JSON key. Composite type columns become nested model classes (flat or nested per the endpoint'snestedannotation). - Async functions wrapping the HTTP call: query-string building (
_queryhelper with URL encoding, repeated keys for arrays, ISO 8601 forDateTime), path-parameter interpolation resolved to the generated request fields, JSON bodies, custom headers, and@body_parameter_namehandling. ApiResult<T>/ApiErrorwrappers carrying the HTTP status code and problem-details errors (IncludeStatusCodeoption or thedartclient_status_codeannotation).- Multipart upload functions for upload endpoints (
http.MultipartRequest, files sent as form fieldfile, optionalprogresscallback reporting uploaded bytes, optional XSRF token header). - Server-sent events (SSE) support: SSE endpoints generate
create{Name}EventSourcefactories returning anSseSubscription(built on streamedpackage:httpresponses — no extra dependency) and SSE-aware functions withonMessage/id/closeAfterMs/awaitConnectionMsparameters that attach the execution-id header to the triggering request — including combined upload+SSE endpoints. - Raw responses for proxies:
proxy_outand void proxy pass-through endpoints returnFuture<http.Response>directly. - Per-call rewrite hooks:
IncludeParseUrlParam/IncludeParseRequestParamoptions (anddartclient_parse_url/dartclient_parse_requestannotations) add optionalparseUrl/parseRequestnamed parameters that can rewrite the URI or request before sending. - Login/logout special-casing: login endpoints return the token
String, logout endpoints returnvoid. - Testability: each generated module exposes a top-level
http.Client? httpClientvariable — assign aMockClientfrompackage:http/testing.dartto intercept requests in tests.
Type mapping
Unlike TypeScript (where every numeric is number), Dart distinguishes integers from floating point:
| PostgreSQL | Dart |
|---|---|
| smallint, integer, bigint | int |
| real, double precision, numeric, money | double |
| boolean | bool |
| json, jsonb | dynamic (configurable via DefaultJsonType) |
| date, timestamp, timestamptz | DateTime (configurable via UseDateTimeType) |
| everything else | String |
| arrays | List<T> |
Dart reserved words and dart:core type names used as parameter or column names are escaped with a trailing underscore (class → class_, double → double_) while JSON keys keep the raw wire names.
Configuration
New NpgsqlRest:DartClientCodeGen configuration section (sibling of ClientCodeGen, which is unchanged). Full section with defaults:
json
{
"NpgsqlRest": {
"DartClientCodeGen": {
"Enabled": false,
"FilePath": null,
"FileOverwrite": true,
"IncludeHost": true,
"CustomHost": null,
"CommentHeader": "Simple",
"CommentHeaderIncludeComments": true,
"BySchema": true,
"IncludeStatusCode": true,
"SeparateModelsFile": false,
"ImportBaseUrlFrom": null,
"HeaderLines": ["// autogenerated at {0}", ""],
"SkipRoutineNames": [],
"SkipFunctionNames": [],
"SkipPaths": [],
"SkipSchemas": [],
"DefaultJsonType": "dynamic",
"UseRoutineNameInsteadOfEndpoint": false,
"ExportUrls": false,
"UniqueModels": false,
"XsrfTokenHeaderName": null,
"ExportEventSources": true,
"IncludeParseUrlParam": false,
"IncludeParseRequestParam": false,
"CustomImports": [],
"CustomHeaders": {},
"IncludeSchemaInNames": true,
"ErrorTypeName": "ApiError",
"ResultTypeName": "ApiResult",
"OmitAutomaticParameters": false,
"ModelPrefix": null,
"ModelSuffix": null,
"UseDateTimeType": true
}
}
}Set Enabled to true and point FilePath at your Flutter project, e.g. "../my_app/lib/api/{0}.dart" — {0} is the PostgreSQL schema name (snake case) or the module name from the dartclient_module annotation (with BySchema).
Most options carry the same meaning as their ClientCodeGen counterparts. Dart-specific options:
SeparateModelsFile— emit request/response (and composite) model classes into a separate{name}_models.dartfile; the client file imports and re-exports it so consumers keep a single import (the Dart analog ofCreateSeparateTypeFile/ExportTypes).ModelPrefix/ModelSuffix— namespace generated model class names to avoid collisions when several generated modules are imported into one file (there is no TypeScript-styleIprefix).UseDateTimeType— when true (default), date/timestamp/timestamptz map toDateTime(DateTime.parseinfromJson,toIso8601StringintoJson); when false, they map toString.ErrorTypeName/ResultTypeName— names of the generated status-wrapper classes (replaces TypeScript's free-textErrorExpression/ErrorType, which cannot be safely injected into Dart).ExportEventSources— when false, SSE event source factories get a private (underscore) name instead of a public one (the Dart analog of not exporting them).IncludeParseUrlParam/IncludeParseRequestParam— add the per-callparseUrl/parseRequestrewrite hooks to every generated function.
ClientCodeGen options with no Dart counterpart (intentionally dropped): SkipTypes (there is no untyped Dart), ExportTypes (every non-underscore Dart declaration is already public), and ImportParseQueryFrom (the generated helpers are underscore-private and cannot be imported across Dart files; use ImportBaseUrlFrom for the shared baseUrl).
Comment annotations
Per-endpoint overrides in routine comments:
dartclient = off— exclude the endpoint from generation.dartclient_module=name— group endpoints into a module file (falls back totsclient_module, so SQL-file directory grouping applies to both generators).dartclient_status_code=true— wrap this endpoint's response inApiResult<T>.dartclient_events=false— generate a plain function for an SSE endpoint (no event-source parameters).dartclient_parse_url=true/dartclient_parse_request=true— add the per-call rewrite hooks to this endpoint.dartclient_export_url=true— also emit a{name}Url()URL-builder function.dartclient_url_only=true— emit only the URL-builder function.
Known limits
numeric/moneymap todouble(the JSON wire format already carries a JSON number); Dartinton Flutter Web is limited to 2^53.- Multi-dimensional arrays are typed as one-dimensional
List<T>(PostgreSQL does not expose array dimensionality — same limitation as the TypeScript client).
Other changes
docker/Dockerfile.jit: the dependency-restore layer now copies theNpgsqlRest.SqlFileSourceandNpgsqlRest.Mcpproject files (previously missing, breakingdotnet restorein the JIT image build), along with the newNpgsqlRest.DartClient.
2. Configurable OpenAPI specification version (NpgsqlRest.OpenApi)
The OpenAPI plugin previously always emitted "openapi": "3.0.3". A new SpecVersion option selects the specification version of the generated document:
jsonc
{
"NpgsqlRest": {
"OpenApiOptions": {
// OpenAPI specification version of the generated document: "3.0" or "3.1".
// "3.0" emits openapi: 3.0.3 (default, backward compatible),
// "3.1" emits openapi: 3.1.1 (JSON Schema 2020-12 alignment).
// This is the spec version, not your API version (that is DocumentVersion).
"SpecVersion": "3.0"
}
}
}"3.0"(default) emitsopenapi: 3.0.3— output is unchanged from previous releases."3.1"emitsopenapi: 3.1.1. The emitter uses no keyword whose semantics differ between OpenAPI 3.0 and 3.1 (nullable,exclusiveMinimum/exclusiveMaximum, ...), so the rest of the document is identical — the setting matters for toolchains that require a 3.1 document (JSON Schema 2020-12 alignment).- Values are trimmed and case-insensitive; any other value fails fast at startup with an error listing the valid values — no silent fallback.
- The emitted version strings are exposed as constants (
OpenApiSpecVersions.V30/V31) instead of inline literals.
3. TanStack Query (React Query) hooks generation (NpgsqlRest.TsClient)
The TypeScript client generator can now emit a TanStack Query v5 hooks module alongside each generated client module: useQuery hooks for GET endpoints and useMutation hooks for everything else, with exported query-key factories. The hooks import the client functions via a relative import and derive all types from them (Parameters<typeof fn>[0] / Awaited<ReturnType<typeof fn>>), so they work regardless of the ExportTypes/CreateSeparateTypeFile settings. Generated code uses TanStack v5 object syntax exclusively and compiles under tsc --strict with only @tanstack/react-query@5 installed.
jsonc
{
"NpgsqlRest": {
"ClientCodeGen": {
"ReactQuery": {
"Enabled": false,
// Output file path for the hooks module. In multi-file mode (BySchema or tsclient_module
// usage) must contain {0}, mirroring FilePath rules. Required when Enabled is true.
"FilePath": null,
"FileOverwrite": true,
// Optional first segment prepended to every query key (for apps consuming multiple
// NpgsqlRest APIs). Null to omit.
"QueryKeyPrefix": null,
// Export query key factory objects alongside hooks.
"ExposeQueryKeys": true,
// Module specifier the hooks import TanStack Query from - point at an internal
// wrapper module when direct imports of @tanstack/react-query are not allowed.
"ImportFrom": "@tanstack/react-query",
"HeaderLines": ["// autogenerated at {0}", ""]
}
}
}
}Generated shape for a GET endpoint (useGetCustomerOrders-style; mutations get use{Name}Mutation with an Omit<UseMutationOptions<...>, "mutationFn"> passthrough):
typescript
export const getCustomerOrdersKeys = {
all: ["getCustomerOrders"] as const,
byRequest: (request: GetCustomerOrdersRequest) =>
["getCustomerOrders", request] as const,
};
export function useGetCustomerOrders(
request: GetCustomerOrdersRequest,
options?: Omit<UseQueryOptions<GetCustomerOrdersResult>, "queryKey" | "queryFn">,
) {
return useQuery({
queryKey: getCustomerOrdersKeys.byRequest(request),
queryFn: () => api.getCustomerOrders(request),
...options,
});
}Details:
- The whole
requestobject is used as a key segment (TanStack v5 hashes keys with stable, key-order-insensitive serialization). tsclient_hooks=offroutine annotation excludes an endpoint from the hooks file only; the client function is still generated.- SSE, upload and url-only endpoints never produce hooks.
- No automatic cache invalidation is generated — wire
onSuccessthrough the options passthrough (a future@invalidatesannotation will build on the exported key factories). - Hooks output is TypeScript-only: with
SkipTypesenabled the run logs a warning and skips hooks generation. ImportFromreplaces the@tanstack/react-queryimport specifier — for teams that route TanStack through an internal wrapper module (the module must re-exportuseQuery,useMutation,UseQueryOptionsandUseMutationOptionswith v5 semantics).ReactQuery.Enabledwith noReactQuery.FilePath(or an emptyImportFrom) fails fast at startup.
4. Function-calling schemas (OpenAI/Anthropic) and llms.txt from the MCP tool set (NpgsqlRest.Mcp)
The MCP plugin can now project its tool catalog (the routines annotated with mcp) into three additional capability documents — without rebuilding any schema logic; every output is a projection of the tool objects the MCP catalog already contains, so parameter exclusion (claim-sourced/IP/virtual/resolved), description precedence, and type mapping are inherited exactly:
- OpenAI Chat Completions
toolsarray —{"type":"function","function":{name, description, parameters}}per tool, whereparametersis the MCPinputSchemaverbatim. - Anthropic Messages API
toolsarray —{name, description, input_schema}per tool, same verbatim schema. - llms.txt — markdown capability document (H1 from
ServerName/database name, blockquote summary fromInstructions, one section per endpoint with method/URL/description/parameters, and a Machine-readable section linking the OpenAPI document, the /mcp endpoint, and the two tools documents).
jsonc
{
"NpgsqlRest": {
"McpOptions": {
"ToolSchemas": {
"Enabled": false,
"FileOverwrite": true,
// null FileName skips writing that file; null UrlPath skips serving that document
"OpenAiFileName": "npgsqlrest_tools_openai.json",
"OpenAiUrlPath": "/tools/openai.json",
"AnthropicFileName": "npgsqlrest_tools_anthropic.json",
"AnthropicUrlPath": "/tools/anthropic.json",
"LlmsTxtFileName": "llms.txt",
"LlmsTxtUrlPath": "/llms.txt"
}
}
}
}Details:
- Independent of the /mcp endpoint: documents are generated and served from
mcpannotations even whenMcpOptions.Enabledis false. - Tool names are sanitized to the OpenAI function-name form (
[a-zA-Z0-9_-], max 64) in both JSON documents, with a logged warning; llms.txt keeps the original names. Two names colliding after sanitization fail fast at startup. - Deterministic output (tools ordered by name, no timestamps): two runs against the same database produce identical bytes.
- Documents are built once at startup and served anonymously (
application/json/text/markdown), same trust level as /openapi.json. - No filtering, no
strict/additionalPropertiesinjection, no invalidation of any existing MCP behavior —HideUnauthorizedTools-style per-user filtering applies to tools/list only.
5. HTTP QUERY method support
The recently standardized HTTP QUERY method — safe and idempotent like GET, but carrying the query in the request body — is now supported across the stack. Annotate a routine with:
sql
comment on function my_schema.search_documents(_query text, _top int) is '
HTTP QUERY
';- Core:
QUERYjoins theMethodenum; theHTTP QUERY [path]annotation (andproxy QUERY/proxy_out QUERY) parse it, routing matches it, and parameters default to the JSON request body (RequestParamType.BodyJson) — the whole point of the method.request_param_type query_stringstill overrides per endpoint. - TypeScript client: emits
method: "QUERY"withbody: JSON.stringify(request)(fetchpasses custom methods through). - React Query hooks: QUERY endpoints map to
useQueryhooks (safe + idempotent = cacheable), not mutations. - Dart client: emits
_send('QUERY', uri, body: jsonEncode(request.toJson()))(package:httpaccepts custom methods). - MCP: QUERY tools get
readOnlyHint: true, same as GET. - HTTP files, llms.txt, SQL file source: work with the method string as-is (SQL files with a select-only body still default to GET; use the
HTTP QUERYannotation to opt in). - OpenAPI: QUERY endpoints are skipped with a logged warning — OpenAPI 3.0/3.1 path items have no
queryoperation key (representable from OpenAPI 3.2, which is not yet an emission target).
6. Required environment variables in the default connection string
The default configuration's connection string now uses the required environment variable syntax ({!NAME}):
json
{
"ConnectionStrings": {
"Default": "Host={!PGHOST};Port=5432;Database={!PGDATABASE};Username={!PGUSER};Password={!PGPASSWORD}"
}
}Running with the default configuration and a missing variable now fails immediately at startup with an error naming exactly what to set — Required environment variable 'PGHOST' (referenced as '{!PGHOST}' in configuration) is not set. — instead of a cryptic downstream Npgsql host-resolution failure.
Supporting changes:
- Connection strings are now resolved through the same substitution as every other configuration value, so the
{!NAME}(required) and{NAME}(optional) syntaxes both work in anyConnectionStringsentry. - The
--configoutput and configuration validation never throw for unset required variables — the literal{!NAME}placeholder is kept in the rendered output, so the configuration can always be inspected before the environment is set up.