// Artifact
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