Skip to content
67 changes: 67 additions & 0 deletions dart/dotprompt/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,73 @@ All notable changes to dotprompt-dart will be documented in this file.

## [Unreleased]

Picoschema now follows the spec and behaves like the other runtimes. Schemas
that relied on the old Dart-only syntax, or on unknown names becoming `$ref`,
need updating.

### Breaking changes

- Picoschema is strict. These now throw `PicoschemaException`:

| Before (1.x) | Now |
| ---------------------------------- | ------------------------------------- |
| `email(the email): string` | `email: string, the email` |
| `tags: string[]` | `tags(array): string` |
| `status: a \| b` | `status(enum): [a, b]` |
| `wild(*): string` | `(*): string` |
| `n: int` (also `str`, `bool`, ...) | `n: integer` |
| `a` and `a?` in the same object | pick one |

- Unknown named schemas throw instead of becoming `{"$ref": name}`. Names are
looked up in `schemas`/`defineSchema`, then `DotpromptOptions.schemaResolver`
(`Dotprompt` and `Picoschema.parse` only; `toJsonSchema` is sync and only
sees `schemas`). Register schemas before converting prompts that use them.
- The `input: Name` / `output: Name` shorthand is parsed as
`{"$type": Name}` (same as `schema: Name`) instead of `{"$ref": Name}`, so
`renderMetadata` resolves it.
- `Picoschema.toJsonSchema`'s `schemas` parameter is now
`Map<String, Map<String, dynamic>>?` (was `Map<String, dynamic>?`).
- `Picoschema.isPicoschema` returns true for anything that is not JSON Schema
(it used to require a bare scalar value). JSON Schema is detected as in the
other runtimes: a top-level `type` naming a JSON Schema type or a
`properties` map, plus list-valued `type`/`anyOf`/`oneOf`/`allOf`/`enum`,
`$schema` and `$ref`. Calling it before `toJsonSchema` is no longer needed.
- `DotpromptOptions.schemas` and `defineSchema` take JSON Schema (as in the
other runtimes), not Picoschema. Registered schemas are inserted as-is;
convert Picoschema with `Picoschema.toJsonSchema` first.

### Fixed

- Picoschema now follows the spec and the JavaScript reference implementation
([genkit-dart#562](https://github.com/genkit-ai/genkit-dart/issues/562)):
- `field(array[, desc]): type`, `field(object[, desc]):` and
`field(enum[, desc]): [...]` produce arrays, objects and enums. Previously
the parenthesized qualifier was treated as a description.
- `(*)` wildcards and every Picoschema form are always converted. Previously
some schemas skipped conversion and were passed through raw.
- Top-level JSON Schema (`type: string`, a bare `properties` map, `anyOf`,
`enum`, `type: [string, "null"]`, etc.) is passed through instead of being
parsed as Picoschema.
- Passed-through JSON Schema and resolved named schemas are deep copies, so
editing the result never changes the input or registered schemas.
- A frontmatter schema with a `$type` field next to other fields is parsed
as a Picoschema object instead of being collapsed to the `$type` value.
- `x?: null` produces `{type: null}` instead of `{type: [null, null]}`.
- `a ?: string` produces a property named `a`, not `a `.
- Named schemas are resolved via `DotpromptOptions.schemaResolver` as well as
`schemas`/`defineSchema`.
- The `input: Name` / `output: Name` shorthand resolves the named schema.
Previously it became a raw `{"$ref": name}`.
- The spec test runner now checks `output` and named `schemas`, so
`spec/picoschema.yaml` is actually enforced.

### Added

- `Picoschema.parse(schema, {schemas, schemaResolver})`, an async variant of
`toJsonSchema` that resolves named schemas through a `SchemaResolver`.

## [1.0.0] - 2026-08-25

### Added

- `renderMetadata` and `compile` now accept an optional `additionalMetadata`
Expand Down
10 changes: 6 additions & 4 deletions dart/dotprompt/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,13 @@ This document tracks feature parity between the Dart and JavaScript (canonical)
| Type scalars (string, integer, etc.) | ✅ | ✅ | |
| Optional fields (`?` suffix) | ✅ | ✅ | |
| Descriptions (`, description`) | ✅ | ✅ | |
| Nested objects | ✅ | ✅ | |
| Array types (`type[]` suffix) | ✅ | ✅ | |
| Enum types | ✅ | ✅ | |
| Nested objects (plain and `(object[, desc])`) | ✅ | ✅ | |
| Arrays (`(array[, desc])`) | ✅ | ✅ | |
| Enums (`(enum[, desc])`) | ✅ | ✅ | |
| Wildcards (`(*)`) | ✅ | ✅ | |
| JSON Schema passthrough | ✅ | ✅ | |
| Named schema references | ✅ | ✅ | |
| Async schema resolution | ✅ | ✅ | |
| Async schema resolution | ✅ | ✅ | `Picoschema.parse` / `DotpromptOptions.schemaResolver` |

## Templating Engine

Expand Down
58 changes: 52 additions & 6 deletions dart/dotprompt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ void main() async {
// Parse and render a prompt
final result = await dotprompt.render('''
---
model: gemini-pro
model: googleai/gemini-flash-latest
config:
temperature: 0.7
---
Expand All @@ -55,18 +55,18 @@ Hello {{name}}! You are a {{role}}.
```dart
final parsed = dotprompt.parse('''
---
model: gemini-pro
model: googleai/gemini-flash-latest
input:
schema:
name: string
age: integer?
age?: integer
default:
name: User
---
Hello {{name}}!
''');

print(parsed.model); // "gemini-pro"
print(parsed.model); // "googleai/gemini-flash-latest"
print(parsed.input?.schema); // Schema definition
```

Expand Down Expand Up @@ -129,6 +129,52 @@ Please analyze this image:
''', DataArgument());
```

### Picoschema

`input.schema` and `output.schema` accept
[Picoschema](https://google.github.io/dotprompt/reference/picoschema/) and are
converted to JSON Schema by `renderMetadata`/`render`:

```yaml
output:
schema:
title: string, the article title
subtitle?: string
tags(array, relevant tags): string
status(enum): [DRAFT, PUBLISHED]
author: Author # named schema
labels(object):
(*): string # additionalProperties
```

Named schemas are JSON Schema, registered up front or resolved on demand:

```dart
final dotprompt = Dotprompt(DotpromptOptions(
schemas: {'Author': authorJsonSchema},
schemaResolver: (name) async => lookupJsonSchema(name),
));

// Or convert directly.
final jsonSchema = await Picoschema.parse(
{'author': 'Author', 'tags(array)': 'string'},
schemaResolver: (name) async => lookupJsonSchema(name),
);
```

A schema that is already JSON Schema is passed through as-is. As in the other
runtimes, that means a top-level `type` naming a JSON Schema type or a
`properties` map (plus `anyOf`/`oneOf`/`allOf`/`enum` lists, `$schema` and
`$ref`):

```yaml
output:
schema:
type: object
properties:
title: {type: string}
```

## API Reference

### Core Classes
Expand All @@ -145,9 +191,9 @@ Please analyze this image:

```dart
final dotprompt = Dotprompt(DotpromptOptions(
defaultModel: 'gemini-pro',
defaultModel: 'googleai/gemini-flash-latest',
modelConfigs: {
'gemini-pro': {'temperature': 0.7},
'googleai/gemini-flash-latest': {'temperature': 0.7},
},
partials: {'...': '...'},
tools: {'...': ToolDefinition(...)},
Expand Down
2 changes: 1 addition & 1 deletion dart/dotprompt/lib/dotprompt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
/// final dotprompt = Dotprompt();
/// final template = '''
/// ---
/// model: gemini-pro
/// model: googleai/gemini-flash-latest
/// ---
/// Hello {{name}}!
/// ''';
Expand Down
46 changes: 30 additions & 16 deletions dart/dotprompt/lib/src/dotprompt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
/// // Parse a template
/// final parsed = dotprompt.parse('''
/// ---
/// model: gemini-pro
/// model: googleai/gemini-flash-latest
/// ---
/// Hello {{name}}!
/// ''');
Expand Down Expand Up @@ -98,7 +98,10 @@ class DotpromptOptions {
/// Pre-registered tool definitions.
final Map<String, ToolDefinition>? tools;

/// Pre-registered schemas (Picoschema or JSON Schema).
/// Pre-registered JSON Schemas, referenced by name from Picoschema.
///
/// Values must already be JSON Schema; they are inserted as-is (same as the
/// other runtimes). Convert Picoschema first with [Picoschema.toJsonSchema].
final Map<String, Map<String, dynamic>>? schemas;

/// Resolver for loading partial templates dynamically.
Expand Down Expand Up @@ -155,7 +158,14 @@ class Dotprompt {
_tools[definition.name] = definition;
}

/// Defines a schema (Picoschema or JSON Schema).
/// Registers a named JSON Schema that Picoschema can reference by name.
///
/// [schema] must already be JSON Schema; it is inserted as-is. To register a
/// schema written in Picoschema, convert it first:
///
/// ```dart
/// dotprompt.defineSchema('Address', Picoschema.toJsonSchema({'street': 'string', 'zip': 'integer'}));
/// ```
void defineSchema(String name, Map<String, dynamic> schema) {
_schemas[name] = schema;
}
Expand Down Expand Up @@ -302,27 +312,22 @@ class Dotprompt {
}
}

// Process schemas (convert Picoschema to JSON Schema)
// Convert Picoschema to JSON Schema. JSON Schema input is passed through by
// the converter itself.
var input = effectiveInput;
var output = effectiveOutput;

if (input?.schema != null && Picoschema.isPicoschema(input!.schema!)) {
final jsonSchema = Picoschema.toJsonSchema(
input.schema,
schemas: _schemas,
);
final inputSchema = input?.schema;
if (input != null && inputSchema != null) {
input = InputConfig(
schema: jsonSchema,
schema: await _resolveSchema(inputSchema),
defaultValues: input.defaultValues,
);
}

if (output?.schema != null && Picoschema.isPicoschema(output!.schema!)) {
final jsonSchema = Picoschema.toJsonSchema(
output.schema,
schemas: _schemas,
);
output = OutputConfig(format: output.format, schema: jsonSchema);
final outputSchema = output?.schema;
if (output != null && outputSchema != null) {
output = OutputConfig(format: output.format, schema: await _resolveSchema(outputSchema));
}

return PromptMetadata(
Expand All @@ -337,6 +342,15 @@ class Dotprompt {
);
}

/// Converts a frontmatter schema to JSON Schema, resolving named schemas from
/// [defineSchema]/[DotpromptOptions.schemas] first and then
/// [DotpromptOptions.schemaResolver].
Future<Map<String, dynamic>> _resolveSchema(Map<String, dynamic> schema) => Picoschema.parse(
schema,
schemas: _schemas,
schemaResolver: _options.schemaResolver,
);

/// Renders a template with the given data.
Future<RenderedPrompt> _renderInternal(
ParsedPrompt parsed,
Expand Down
4 changes: 2 additions & 2 deletions dart/dotprompt/lib/src/models/parsed_prompt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import "prompt_metadata.dart";
/// ```dart
/// final source = '''
/// ---
/// model: gemini-pro
/// model: googleai/gemini-flash-latest
/// config:
/// temperature: 0.7
/// ---
Expand All @@ -43,7 +43,7 @@ import "prompt_metadata.dart";
///
/// final parsed = Parser.parseDocument(source);
/// print(parsed.template); // "Hello {{name}}!"
/// print(parsed.model); // "gemini-pro"
/// print(parsed.model); // "googleai/gemini-flash-latest"
/// ```
@immutable
class ParsedPrompt {
Expand Down
19 changes: 12 additions & 7 deletions dart/dotprompt/lib/src/models/prompt_metadata.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import "parsed_prompt.dart";
///
/// ```yaml
/// ---
/// model: gemini-pro
/// model: googleai/gemini-flash-latest
/// config:
/// temperature: 0.7
/// maxOutputTokens: 1024
Expand Down Expand Up @@ -200,11 +200,13 @@ class InputConfig {

/// Creates an [InputConfig] from a value that can be a String or Map.
///
/// If the value is a String, it's treated as a schema name reference.
/// If the value is a String (`input: MySchema`), it's treated as a Picoschema
/// type string, so named schemas are resolved during metadata resolution.
factory InputConfig.fromValue(dynamic value) {
if (value is String) {
// String value is a schema name reference
return InputConfig(schema: {r"$ref": value});
// Same wrapping as `schema: MySchema` in fromJson, so the name is
// resolved (or rejected) instead of passing through as a raw `$ref`.
return InputConfig(schema: {r"$type": value});
} else if (value is Map<String, dynamic>) {
return InputConfig.fromJson(value);
}
Expand Down Expand Up @@ -259,11 +261,14 @@ class OutputConfig {

/// Creates an [OutputConfig] from a value that can be a String or Map.
///
/// If the value is a String, it's treated as a schema name reference.
/// If the value is a String (`output: MySchema`), it's treated as a
/// Picoschema type string, so named schemas are resolved during metadata
/// resolution.
factory OutputConfig.fromValue(dynamic value) {
if (value is String) {
// String value is a schema name reference
return OutputConfig(schema: {r"$ref": value});
// Same wrapping as `schema: MySchema` in fromJson, so the name is
// resolved (or rejected) instead of passing through as a raw `$ref`.
return OutputConfig(schema: {r"$type": value});
} else if (value is Map<String, dynamic>) {
return OutputConfig.fromJson(value);
}
Expand Down
2 changes: 1 addition & 1 deletion dart/dotprompt/lib/src/models/rendered_prompt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import "../types.dart";
/// final dotprompt = Dotprompt();
/// final result = dotprompt.render(template, data);
///
/// print(result.config['model']); // "gemini-pro"
/// print(result.config['model']); // "googleai/gemini-flash-latest"
/// for (final message in result.messages) {
/// print('${message.role}: ${message.content}');
/// }
Expand Down
6 changes: 3 additions & 3 deletions dart/dotprompt/lib/src/parse.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
///
/// ```
/// ---
/// model: gemini-pro
/// model: googleai/gemini-flash-latest
/// config:
/// temperature: 0.7
/// ---
Expand Down Expand Up @@ -60,13 +60,13 @@ final RegExp _frontmatterPattern = RegExp(
/// ```dart
/// final source = '''
/// ---
/// model: gemini-pro
/// model: googleai/gemini-flash-latest
/// ---
/// Hello {{name}}!
/// ''';
///
/// final parsed = Parser.parseDocument(source);
/// print(parsed.model); // "gemini-pro"
/// print(parsed.model); // "googleai/gemini-flash-latest"
/// print(parsed.template); // "Hello {{name}}!"
/// ```
class Parser {
Expand Down
Loading
Loading