most_mapper 0.3.0 copy "most_mapper: ^0.3.0" to clipboard
most_mapper: ^0.3.0 copied to clipboard

A YAML-driven CLI generator for Dart and C# model mapping code.

MOST Mapper

most logo

pub version

most_mapper is a Dart CLI package that generates Dart and C# mapping code from one YAML file. The mapping file declares data models, enums, tagged unions, converter expressions, and typed mappings between models. Generated Dart files are formatted with dart format; generated C# files are formatted with dotnet format.

Usage #

Generate both Dart and C#:

dart run most_mapper \
  --mapping example/basic/mapping.yaml \
  --dart-out-dir example/basic/output/dart \
  --dart-file-name models_mapper.g.dart \
  --csharp-out-dir example/basic/output/csharp \
  --csharp-file-name ModelsMapper.g.cs

Generate Dart only:

dart run most_mapper --mapping mapping.yaml --dart-out-dir output/dart

Generate C# only:

dart run most_mapper --mapping mapping.yaml --csharp-out-dir output/csharp

Defaults:

CLI option Default
--dart-file-name mapper.g.dart
--csharp-file-name Mapper.g.cs

--mapping is required. At least one of --dart-out-dir or --csharp-out-dir must be present. C# generation requires the .NET SDK because the generated .cs file is passed through dotnet format.

YAML Features #

  • models declares generated data classes, enums, and tagged unions.
  • json: true on a data model or union generates JSON helpers. It defaults to false.
  • Fields support field: Type, field: Type?, and { type: Type, nullable: true, doc: Description }.
  • Enum models support string and int wire values.
  • Enums can map to and from String and int when every enum value declares that wire value.
  • converters define trusted Dart and C# expressions emitted as public helper methods.
  • Converter name is optional. Use a name only when a mapping must select a specific non-default converter.
  • Field mappings can use { from: SourceField, converter: converterName } to force a named converter, or { from: SourceField, converter: default } to force a built-in default converter when available.
  • Field mappings can use { parameter: Type } to add a required parameter to the generated mapping method.
  • DateTime has default DateTime -> String and String -> DateTime converters using UTC ISO text like 2026-06-24T07:19:06Z.
  • Multiline converter expressions are supported with YAML block strings.
  • mappings generate typed extension methods on source models.
  • Mapping fields support { from: SourceField }, { parameter: Type }, { const: null }, and scalar constants.
  • Mapping field names are case-sensitive YAML keys.

Tagged Unions #

Unions use Type as their discriminator unless discriminator is specified. Dart emits a sealed base class and final variants; C# emits an abstract base class and sealed variants. JSON-enabled unions reject missing or unknown discriminator values.

models:
  StockSource:
    json: true
    union:
      discriminator: Type
      variants:
        BarsetsSource:
          value: Barsets
          fields:
            BarsetIds: List<int>
        PackingPlanSource:
          value: PackingPlan

Union types can be used as model fields and converter input/output types. Use explicit converters when domain and wire unions have different variant class names.

Data Types #

YAML type Dart type C# type Notes
String String string Basic scalar
bool bool bool Basic scalar
int int int Numeric cast source/target
double double double Numeric cast source/target
num num double Conservative numeric support
decimal double decimal Wire/business decimal support
DateTime DateTime DateTime Default JSON text is UTC ISO, e.g. 2026-06-24T07:19:06Z
List<T> List<T> List<T> Element-wise mapping/conversion
custom model generated class generated class Declared in models
enum model generated enum generated enum Supports string/int scalar conversion
union model sealed class abstract class Closed variants with fixed discriminator values

Mapping Rules #

  • Default mapping assigns target fields from source fields with the same YAML key when the value is compatible.
  • Compatible means identical, numeric-castable, enum-scalar convertible, model-mappable, list-compatible, or converter-backed.
  • Every target field must be assigned by default mapping or an explicit mapping entry.
  • Parameter mapping keys are both the target field name and the generated parameter name.
  • Parameter mappings use the same conversion and converter resolution as source-field mappings.
  • const: null is only valid for nullable target fields.
  • Scalar constants are validated against the target field type.
  • The generator fails before writing output if validation fails.

Converter Expressions #

Converters are emitted into public helper methods in the generated file. Treat the mapping YAML as trusted input. Converter names are optional. If multiple converters have the same from and to types, the last converter in the file is used by default; mappings can still reference any named converter explicitly. The name default is reserved.

Each converter has from, to, dart.expression, and csharp.expression; name is optional. The generated helper method passes the input value as a parameter named source; expressions should return the converted value.

converters:
  - name: offsetDateTimeToString
    from: DateTime
    to: String
    dart:
      expression: |
        (() {
          String two(int value) => value.toString().padLeft(2, '0');
          final offset = source.timeZoneOffset;
          final sign = offset.isNegative ? '-' : '+';
          final absoluteOffset = offset.abs();
          return '${source.year.toString().padLeft(4, '0')}-${two(source.month)}-${two(source.day)}T${two(source.hour)}:${two(source.minute)}:${two(source.second)}$sign${two(absoluteOffset.inHours)}:${two(absoluteOffset.inMinutes.remainder(60))}';
        })()
    csharp:
      usings: ["System", "System.Globalization"]
      expression: |
        new DateTimeOffset(source).ToString("yyyy-MM-dd'T'HH:mm:sszzz", CultureInfo.InvariantCulture)

  - name: offsetStringToDateTime
    from: String
    to: DateTime
    dart:
      expression: |
        (() {
          final match = RegExp(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})([+-])(\d{2}):(\d{2})$').firstMatch(source);
          if (match == null) {
            throw FormatException('Expected yyyy-MM-ddTHH:mm:ss+XX:XX', source);
          }
          final withoutOffsetUtc = DateTime.parse('${match.group(1)}Z');
          final offset = Duration(
            hours: int.parse(match.group(3)!),
            minutes: int.parse(match.group(4)!),
          );
          return match.group(2) == '+'
              ? withoutOffsetUtc.subtract(offset)
              : withoutOffsetUtc.add(offset);
        })()
    csharp:
      usings: ["System", "System.Globalization"]
      expression: |
        DateTimeOffset.ParseExact(
            source,
            "yyyy-MM-dd'T'HH:mm:sszzz",
            CultureInfo.InvariantCulture
        ).UtcDateTime

The example emits offset text like 2026-06-24T07:19:06+00:00. When reading text back, the timestamp part is treated as UTC, then +HH:mm offsets are subtracted and -HH:mm offsets are added so the returned DateTime represents the same instant in UTC.

Example Mapping YAML #

models:
  Measurement:
    doc: Sample scaled numeric value.
    json: true
    fields:
      code: String
      scale: int
      value: int

  OrderStatus:
    enum:
      pending: { string: pending, int: 0 }
      captured: { string: captured, int: 1 }
      failed: { string: failed, int: 2 }

  ModelA:
    doc: Domain model.
    json: true
    fields:
      JsonFieldName: { type: String, nullable: true }
      reading: Measurement
      status: OrderStatus
      bs: List<ModelB>
      createdAt: DateTime?

  ModelAWire:
    doc: Wire model.
    json: true
    fields:
      Id: String?
      reading: decimal
      status: String
      statusCode: int
      bs: List<ModelBWire>
      createdAt: String?
      SomeField: String?

  ModelB:
    doc: Domain child model.
    json: true
    fields:
      Id: String
      Datetime: DateTime

  ModelBWire:
    doc: Wire child model.
    json: true
    fields:
      Id: String
      Datetime: String

converters:
  - from: Measurement
    to: decimal
    dart:
      imports: ["dart:math"]
      expression: "source.value / pow(10, source.scale)"
    csharp:
      usings: ["System"]
      expression: "(decimal)source.Value / (decimal)Math.Pow(10, source.Scale)"

  - name: offsetDateTimeToString
    from: DateTime
    to: String
    dart:
      expression: |
        (() {
          String two(int value) => value.toString().padLeft(2, '0');
          final offset = source.timeZoneOffset;
          final sign = offset.isNegative ? '-' : '+';
          final absoluteOffset = offset.abs();
          return '${source.year.toString().padLeft(4, '0')}-${two(source.month)}-${two(source.day)}T${two(source.hour)}:${two(source.minute)}:${two(source.second)}$sign${two(absoluteOffset.inHours)}:${two(absoluteOffset.inMinutes.remainder(60))}';
        })()
    csharp:
      usings: ["System", "System.Globalization"]
      expression: |
        new DateTimeOffset(source).ToString("yyyy-MM-dd'T'HH:mm:sszzz", CultureInfo.InvariantCulture)

  - name: offsetStringToDateTime
    from: String
    to: DateTime
    dart:
      expression: |
        (() {
          final match = RegExp(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})([+-])(\d{2}):(\d{2})$').firstMatch(source);
          if (match == null) {
            throw FormatException('Expected yyyy-MM-ddTHH:mm:ss+XX:XX', source);
          }
          final withoutOffsetUtc = DateTime.parse('${match.group(1)}Z');
          final offset = Duration(
            hours: int.parse(match.group(3)!),
            minutes: int.parse(match.group(4)!),
          );
          return match.group(2) == '+'
              ? withoutOffsetUtc.subtract(offset)
              : withoutOffsetUtc.add(offset);
        })()
    csharp:
      usings: ["System", "System.Globalization"]
      expression: |
        DateTimeOffset.ParseExact(
            source,
            "yyyy-MM-dd'T'HH:mm:sszzz",
            CultureInfo.InvariantCulture
        ).UtcDateTime

mappings:
  - from: ModelB
    to: ModelBWire
    fields:
      Datetime: { from: Datetime, converter: offsetDateTimeToString }

  - from: ModelBWire
    to: ModelB
    fields:
      Datetime: { from: Datetime, converter: offsetStringToDateTime }

  - from: ModelA
    to: ModelAWire
    fields:
      Id: { from: JsonFieldName }
      reading: { from: reading }
      status: { from: status }
      statusCode: { from: status }
      createdAt: { from: createdAt, converter: default }
      SomeField: { const: null }

Validation Failures #

The generator stops before writing output when it finds errors such as:

  • Unknown models, fields, or types.
  • Missing converters or mappings for incompatible fields.
  • Duplicate enum wire values.
  • Invalid constants.
  • Unsafe nullable-to-non-nullable assignments.
  • Generated Dart or C# identifier collisions.

Assumptions #

  • One YAML file is the v1 source of truth. There are no separate --spec files.
  • models is the only type declaration section. Enums are declared inside models.
  • YAML scalar constants only are supported for { const: ... } in v1.
  • Raw converter expressions are emitted into generated public helper methods and are not sandboxed.
  • The built-in DateTime converters use UTC ISO text ending in Z; custom DateTime converters can override that format.
  • Mapping field names are case-sensitive YAML keys.
  • Output is one generated file per requested target language.
  • Generated Dart and C# files are formatted before the CLI exits.

Development #

dart test
dart analyze
dart format --set-exit-if-changed --line-length 120 .
1
likes
160
points
17
downloads

Documentation

API reference

Publisher

verified publishermost.io

Weekly Downloads

A YAML-driven CLI generator for Dart and C# model mapping code.

Repository (GitHub)
View/report issues

Topics

#codegen #mapper #yaml

License

MIT (license)

Dependencies

args, path, recase, yaml

More

Packages that depend on most_mapper