// Artifact

Pub

Map Dart objects to maps, JSON, YAML, TOML, XML, or properties files effortlessly! No part files, no manual boilerplate, no extending or mixing in nonexistent classes—just slap the @Artifact annotation on any class, run the code generator, and get automatic extension methods for serialization, deserialization, immutability with copyWith, and more.

The beauty of Artifact lies in its simplicity: annotate your immutable classes, and the generator creates everything you need. Deserialization is as easy as $YourClass.fromMap(map), returning an instance of YourClass. Supports inheritance, collections, enums, custom field renaming, and even attachments for metadata like UI components.

Features

  • Zero Boilerplate: Just @Artifact—no part files or manual implementations.
  • Multi-Format Support: Serialize to JSON, YAML, TOML, XML, or properties.
  • Inheritance: Automatic subclass handling with _subclass_ markers in maps.
  • Collections: Full support for List, Set, Map (including nested artifacts).
  • Enums: Serialized by name, deserialized by value.
  • CopyWith: Immutable updates with deltas for numerics, append/remove for collections.
  • Attachments: Annotate fields with custom metadata (e.g., UI hints) and retrieve via getAttachment.
  • Options: Compression for smaller output, reflection for runtime introspection, schema generation.
  • Type Safety: Explicit typing, null safety, and error handling for missing required fields.

Because artifact is generating extension methods, certain class ops are not possible such as:

  • Read-only fields
  • JSON templates that are NOT object
  • Getter properties
  • Private fields

Setup

Add the runtime dependency:

flutter pub add artifact

Add the code generator and builder:

flutter pub add artifact_gen build_runner --dev

Run the generator:

dart run build_runner build

This generates lib/gen/artifacts.gen.dart with extensions for your annotated classes. Import it in your code: import 'gen/artifacts.gen.dart';.

For watch mode during development:

dart run build_runner watch

Basic Usage

Annotate your immutable class with @Artifact. Fields must be final and use a const constructor with named parameters.

import 'package:artifact/artifact.dart';
import 'gen/artifacts.gen.dart'; // Generated

@artifact // That's it!
class Animal {
  @rename("hp") // Optional: custom map key
  final int health;
  final String? nonSerializable; // Ignored in serialization
  
  // The constructor defines what is serialized!
  const Animal({this.health = 100});
}

Serialization

Animal dog = const Animal(health: 125);

String json = dog.toJson(); // Or toYaml(), toToml(), toXml(), toProperties()
Map<String, dynamic> map = dog.toMap();

print(json); // {"hp": 125}

Deserialization

Use the generated extension:

Map<String, dynamic> data = {"hp": 125};
Animal animal = $Animal.fromMap(data); // Returns Animal instance

// Handles defaults and required fields (throws if missing required)
Animal cat = $Cat.fromMap({"hp": 70, "lives": 9}); // For subclasses

Inheritance

Subclasses are automatically handled. The generated map includes a _subclass_BaseClass key for polymorphic deserialization.

@artifact
class Dog extends Animal {
  final bool goodBoy;

  const Dog({
    super.health = 125,
    this.goodBoy = true,
  });
}

@artifact
class Cat extends Animal {
  final int lives;

  const Cat({
    super.health = 70,
    this.lives = 9,
  });
}

@artifact
class World {
  final List<Animal> animals; // Polymorphic list

  const World({this.animals = const []});
}

Usage:

World world = World(
  animals: [
    const Dog(goodBoy: true),
    const Cat(lives: 8, health: 50),
  ],
);

print(world.toJson(pretty: true));

Output:

{
  "animals": [
    {
      "_subclass_Animal": "Dog",
      "hp": 125,
      "goodBoy": true
    },
    {
      "_subclass_Animal": "Cat",
      "hp": 50,
      "lives": 8
    }
  ]
}

Deserialization reconstructs the correct subclass:

World fromWorld = $World.fromMap(worldMap);
Animal first = fromWorld.animals.first; // Dog instance

Collections and Enums

Supports nested artifacts, primitives, and collections. Enums serialize by name.

enum Mood { happy, sad }

@artifact
class Pet {
  final String name;
  final Mood mood;
  final List<Animal> friends;
  final Set<String> tags;
  @rename("data")
  final Map<String, int> metadata;

  const Pet({
    required this.name,
    required this.mood,
    required this.friends,
    this.tags = const {},
    this.metadata = const {},
  });
}

Serialization handles nesting and collections automatically.

Immutability with CopyWith

Generated copyWith supports resets, direct values, and deltas (for numerics). Collections support append/remove.

Animal dog = const Dog(health: 100, goodBoy: true);

// Add health, toggle goodBoy
Animal updated = dog.copyWith(deltaHealth: 25, goodBoy: false);

// Reset health to default
Animal reset = dog.copyWith(resetHealth: true);

// For lists/sets
World updatedWorld = world.copyWith(
  appendAnimals: [newCat],
  removeTags: {'oldTag'},
);

Attachments

Attach constant metadata to classes or fields for runtime retrieval (e.g., UI hints).

First, define attachment types:

enum UIType { title, subtitle }

class UITitle extends Attachment {
  const UITitle([super.data = UIType.title]);
}

const uiTitle = UITitle();
const uiSubtitle = UISubtitle();

Annotate:

@Artifact
@attach(uiTitle) // Class-level
class Person {
  @uiTitle
  final String name;

  @uiSubtitle
  final String email;

  const Person({required this.name, required this.email});
}

Retrieve:

Person person = const Person(name: "Alice", email: "alice@example.com");

// Field attachment
String title = person.getAttachment<UIType, String>(UIType.title); // "Alice"

// Class attachment
List<dynamic> allAttachments = $Person.rootAttachments;

Advanced Options

Customize via @Artifact parameters:

@Artifact(
  compression: true, // Minify generated code (default: true)
  reflection: true,  // Generate runtime introspection (e.g., $fields)
  generateSchema: true, // Generate JSON schema
)
class MyClass { ... }
  • Reflection: Access fields via $MyClass.$fields (List of field descriptors).
  • Schema: Generates $MyClass.schema for validation.

Custom codecs via @codec:

@codec(MyCustomCodec())
class MyClass { ... }

Generated API

For each @Artifact class MyClass, the generator creates an extension $MyClass:

  • Serialization: toJson([pretty]), toYaml(), toToml(), toXml([pretty]), toProperties(), toMap().
  • Deserialization: static MyClass fromJson(String), fromYaml(String), etc., fromMap(Map<String, dynamic>).
  • Immutability: copyWith({params}) with deltas/append/remove.
  • Helpers: static MyClass newInstance (defaults), $fields (if reflection), schema (if generateSchema).
  • Global: $isArtifact(obj), $constructArtifact<MyClass>(), $artifactToMap(obj), $artifactFromMap<MyClass>(map).

Example

See example/lib/example.dart for a full demo with strings, ints, enums, subclasses, lists, sets, and maps. Run:

cd example
dart run build_runner build
dart run

Output demonstrates construction, serialization, and type checks.

How It Works

The artifact_gen builder scans for @Artifact classes, analyzes fields/constructors/inheritance, and generates type-safe extensions. Supports null safety, required fields (throws on missing), defaults, and compression for concise code.

No runtime reflection—pure code generation for performance.

Limitations

  • Only serializes fields defined in the constructor.
  • Polymorphism requires all subclasses annotated.
  • Custom types need manual codec registration if not primitives/artifacts/enums.

Interesting Notes

  • You can mark your existing dart_mappable / freezed / json_serializable classes with @artifact to get dual functionality or for ease of migration
  • Artifact actually does support mutable fields its just not recommended
  • You can customize @Artifact by creating a subclass of it and define default params, then do const Artifact art = Artifact(...); then @art

Libraries

artifact
codec
shrink