Pub

A general-purpose Dart library for generating type-safe data classes and runtime-accessible JSON Schemas from abstract class definitions.

Features

  • Type-Safe Data Classes: Generates fully-typed Dart data classes from simple abstract definitions.
  • Runtime JSON Schema: Access the standard JSON Schema for any generated type at runtime.
  • Serialization: Built-in toJson and parse methods.
  • Validation: Validate JSON data against the generated schema at runtime.
  • Recursive Schemas: Easy support for recursive data structures (e.g., trees) using $ref.

Installation

Add schemantic and build_runner to your pubspec.yaml:

dart pub add schemantic
dart pub add dev:build_runner

Usage

1. Define your specific Schema

Create a Dart file (e.g., user.dart) and define your schema as an abstract class annotated with @Schematic().

import 'package:schemantic/schemantic.dart';

part 'user.schema.g.dart';

@Schematic()
abstract class UserSchema {
  String get name;
  int? get age;
  bool get isAdmin;
}

2. Generate Code

Run the build runner to generate the implementation:

dart run build_runner build

This will generate a user.schema.g.dart file containing:

  • User: The concrete data class.
  • UserType: A utility class for parsing, schema access, and validation.

3. Use the Generated Types

You can now use the generated User class and UserType utility:

void main() async {
  // Create an instance using the generated class
  final user = User.from(
    name: 'Alice',
    age: 30,
    isAdmin: true,
  );

  // Serialize to JSON
  print(user.toJson()); 
  // Output: {name: Alice, age: 30, isAdmin: true}

  // Parse from JSON
  final parsed = UserType.parse({
    'name': 'Bob',
    'isAdmin': false,
  });
  print(parsed.name); // Bob

  // Access JSON Schema at runtime
  final schema = UserType.jsonSchema();
  print(schema.toJson()); 
  // Output: {type: object, properties: {name: {type: string}, ...}, required: [name, isAdmin]}
  
  // Validate data
  final validation = await schema.validate({'name': 'Charlie'}); // Missing 'isAdmin'
  if (validation.isNotEmpty) {
    print('Validation errors: $validation');
  }
}

Advanced

Recursive Schemas

For recursive structures like trees, use the useRefs: true option when generating the schema. This utilizes JSON Schema $ref to handle recursion.

@Schematic()
abstract class NodeSchema {
  String get id;
  List<NodeSchema>? get children;
}
void main() {
  // Must use useRefs: true for recursive schemas
  final schema = NodeType.jsonSchema(useRefs: true);
  
  print(schema.toJson());
  // Generates schema with "$ref": "#/$defs/Node"
}

Basic & Dynamic Types

Schemantic provides a set of basic types and helpers for creating dynamic schemas without generating code.

Primitives

  • stringType()
  • intType()
  • doubleType()
  • boolType()
  • voidType()

listType and mapType

You can create strongly typed Lists and Maps dynamically:

void main() {
  // Define a List of Strings
  final stringList = listType(stringType());
  print(stringList.parse(['a', 'b'])); // ['a', 'b']

  // Define a Map with String keys and Integer values
  final scores = mapType(stringType(), intType());
  print(scores.parse({'Alice': 100, 'Bob': 80})); // {'Alice': 100, 'Bob': 80}

  // Nesting types
  final matrix = listType(listType(intType()));
  print(matrix.parse([[1, 2], [3, 4]])); // [[1, 2], [3, 4]]
  
  // JSON Schema generation works as expected
  print(scores.jsonSchema().toJson());
  // {type: object, additionalProperties: {type: integer}}
}

## Schema Metadata

You can add a description to your generated schema using the `description` parameter in `@Schematic`:

```dart
@Schematic(description: 'Represents a user in the system')
abstract class UserSchema {
  // ...
}

Enhanced Collections

You can use listType and mapType to create collections with metadata and validation:

// A list of strings with description and size constraints.
final tags = listType(
  stringType(),
  description: 'A list of tags',
  minItems: 1,
  maxItems: 10,
  uniqueItems: true,
);

// A map with integer values.
final scores = mapType(
  stringType(),
  intType(),
  description: 'Player scores',
  minProperties: 1,
);

Basic Types

Schemantic provides factories for basic types with optional metadata:

  • stringType({String? description, int? minLength, ...})
  • intType({String? description, int? minimum, ...})
  • doubleType({String? description, double? minimum, ...})
  • boolType({String? description})
  • dynamicType({String? description})

Example:

final age = intType(
  description: 'Age in years',
  minimum: 0,
);

Customizing Fields

You can use specialized annotations to apply JSON Schema constraints directly to your Dart fields.

  • @Field: Basic customization (name, description).
  • @StringField: Constraints for strings (minLength, maxLength, pattern, format, enumValues).
  • @IntegerField: Constraints for integers (minimum, maximum, multipleOf).
  • @NumberField: Constraints for doubles/numbers (minimum, maximum, multipleOf).
@Schematic()
abstract class UserSchema {
  // Map 'age' to 'years_old' in JSON, and add validation
  @IntegerField(
    name: 'years_old', 
    description: 'Age of the user',
    minimum: 0,
    maximum: 120
  )
  int? get age;

  @StringField(
    minLength: 2,
    maxLength: 50,
    pattern: r'^[a-zA-Z\s]+$',
    enumValues: ['user', 'admin'] // Mapped to 'enum' in JSON Schema
  )
  String get role;
}

Validation matches the Dart type (e.g., using @StringField on an int getter will throw a build-time error).

Schema-based Definition

Alternatively, you can define your schema using the Schema class directly. This is useful when you want to define the schema structure explicitly without an abstract class.

import 'package:schemantic/schemantic.dart';

part 'config.schema.g.dart';

// The variable name determines the generated class name (e.g. 'myConfig' -> 'MyConfig').
const myConfig = Schema.object(
  properties: {
    'host': Schema.string(),
    'port': Schema.integer(minimum: 1, maximum: 65535),
    'tags': Schema.list(items: Schema.string()),
    'meta': Schema.map(valueType: Schema.string()), // or Schema.object(additionalProperties: Schema.string())
  },
  required: ['host', 'port'],
);

Run build_runner, and it will generate:

  • MyConfig class (extension type)
  • myConfigType factory and utility

Strict Generation

The generator enforces strict validation for Schema definitions:

  • You must use Schema.* static methods (e.g., Schema.object, Schema.string).
  • Property keys must be string literals.
  • Unknown arguments will throw an error during generation.

Libraries

builder
schemantic