Skip to content

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():

  1. The property looks up PropertySystem.getConverter<T>(customKey)
  2. The converter's toJson(value) method converts the typed value to a JSON-compatible representation
  3. The result is returned as dynamic (typically String, int, double, bool, List, or Map)

Deserialization with property.fromJson(json) follows the reverse path.

JsonConverter<T> Interface

dart
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:

dart
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

ConverterTypeJSON Format
StringJsonConverterString"text"
IntJsonConverterint42
DoubleJsonConverterdouble3.14
BoolJsonConverterbooltrue
DateTimeJsonConverterDateTime"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)
UnionJsonConverterString"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:

dart
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:

dart
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

dart
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:

dart
PropertySystem.register<Color?>(
  converter: NullableJsonConverter<Color>(ColorJsonConverter()),
);

List Converter

Compose with an item converter:

dart
PropertySystem.register<List<String>>(
  converter: ListJsonConverter<String>(StringJsonConverter()),
);

Registering Converters

dart
// 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:

dart
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