JSON Serialization
The Property System uses a registry-based JsonConverter<T> system for type-safe serialization and deserialization of property values.
How It Works
When you call property.toJson():
- The property looks up
PropertySystem.getConverter<T>(customKey) - The converter's
toJson(value)method converts the typed value to a JSON-compatible representation - The result is returned as
dynamic(typicallyString,int,double,bool,List, orMap)
Deserialization with property.fromJson(json) follows the reverse path.
JsonConverter<T> Interface
abstract interface class JsonConverter<T> {
T fromJson(dynamic json);
dynamic toJson(T value);
T get defaultValue;
}BaseJsonConverter<T>
An abstract base class that adds error handling:
abstract base class BaseJsonConverter<T> implements JsonConverter<T> {
const BaseJsonConverter();
T convertFromJson(dynamic json); // override this
dynamic convertToJson(T value); // override this
// fromJson() and toJson() wrap these with try/catch
// and throw JsonConversionException on failure
}Built-in Converters
| Converter | Type | JSON Format |
|---|---|---|
StringJsonConverter | String | "text" |
IntJsonConverter | int | 42 |
DoubleJsonConverter | double | 3.14 |
BoolJsonConverter | bool | true |
DateTimeJsonConverter | DateTime | "2026-03-26T14:30:00.000" (ISO 8601) |
NullableJsonConverter<T> | T? | Wraps inner converter, passes null through |
EnumJsonConverter<T> | T extends Enum | "enumName" (via .name) |
EnumOptionsJsonConverter<T> | T | "valueName" (matches by name, toString, or index) |
ListJsonConverter<T> | List<T> | [...] (delegates items to inner converter) |
UnionJsonConverter | String | "selectedKey" (union handles full structure) |
All default types are auto-registered when PropertySystem is first accessed.
Collection Serialization
PropertyCollection provides toJson() and fromJson() for the entire collection:
final collection = buildCourseSettings();
// Serialize all properties
final json = collection.toJson();
// {
// "title": "Flutter Fundamentals",
// "slug": "flutter_fundamentals",
// "duration": 40,
// "level": "beginner",
// "enrollmentType": "open",
// "featured": false,
// "tags": ["flutter", "dart"]
// }
// Restore from JSON
collection.fromJson(json);Properties not present in the JSON are skipped during fromJson(). Extra keys in the JSON that do not match a property key are ignored.
UnionProperty Serialization
UnionProperty handles its own serialization with a two-key structure:
final scheduling = UnionProperty(
key: 'scheduling',
label: 'Mode',
defaultSelectedKey: 'fixed',
options: [
UnionOption(key: 'fixed', title: 'Fixed',
property: StringProperty(key: 'date', label: 'Date')),
UnionOption(key: 'flexible', title: 'Flexible',
property: IntProperty(key: 'days', label: 'Days', defaultValue: 30)),
],
);
scheduling.selectOption('flexible');
print(scheduling.toJson());
// {"selectedKey": "flexible", "selectedValue": 30}Creating Custom Converters
Simple Converter
class ColorJsonConverter extends BaseJsonConverter<Color> {
const ColorJsonConverter();
@override
Color get defaultValue => Colors.blue;
@override
Color convertFromJson(dynamic json) {
if (json is int) return Color(json);
if (json is String) return Color(int.parse(json.replaceFirst('#', '0xFF')));
throw ArgumentError('Cannot convert $json to Color');
}
@override
dynamic convertToJson(Color value) => '#${value.value.toRadixString(16)}';
}Nullable Wrapper
Wrap any converter to handle nullable values:
PropertySystem.register<Color?>(
converter: NullableJsonConverter<Color>(ColorJsonConverter()),
);List Converter
Compose with an item converter:
PropertySystem.register<List<String>>(
converter: ListJsonConverter<String>(StringJsonConverter()),
);Registering Converters
// Register with both editor and converter
PropertySystem.register<Color>(
editor: const ColorPropertyEditor(),
converter: const ColorJsonConverter(),
);
// Register converter only (for headless/server use)
PropertySystem.register<Color>(
converter: const ColorJsonConverter(),
);
// Register with custom key
PropertySystem.register<String>(
key: 'markdown',
converter: const MarkdownJsonConverter(),
);Custom-key lookup takes priority over type lookup when a property sets customKey. This lets one value type use multiple serialization or editor variants, such as readonly:string, field_selector, or an app-specific markdown editor/converter.
Error Handling
JsonConversionException is thrown when conversion fails:
try {
property.fromJson('not-a-number');
} on JsonConversionException catch (e) {
print(e.message); // 'Failed to convert JSON to int'
print(e.json); // 'not-a-number'
print(e.targetType); // int
print(e.cause); // FormatException
}Next Steps
- Custom Editors -- Pair converters with custom editors
- Integration Guide -- How serialization is used in practice
- API Reference: JSON Converters -- Full converter API