OmniMapper

pub package License

A powerful, highly customizable code-generation library for Dart and Flutter that automatically generates object-to-object mapping code.

OmniMapper eliminates the boilerplate of manually writing conversion methods between your application layers (e.g., ModelEntity, DTOViewModel), keeping your codebase clean and reducing bugs.

Think of it as the AutoMapper/MapStruct for the Dart ecosystem.

Installation

Add the following to your pubspec.yaml:

dependencies:
  omni_mapper: ^0.1.0

dev_dependencies:
  build_runner: ^2.4.0
  omni_mapper_generator: ^0.1.0

Then run:

dart pub get

Quick Start

1. Annotate your class

import 'package:omni_mapper/omni_mapper.dart';

part 'user_model.g.dart';

class UserEntity {
  final int id;
  final String name;
  UserEntity({required this.id, required this.name});
}

@OmniMapper(target: UserEntity)
class UserModel {
  final int id;
  final String name;
  UserModel({required this.id, required this.name});
}

2. Run the generator

dart run build_runner build -d

3. Use the generated code

final model = UserModel(id: 1, name: 'John');
final entity = model.toEntity(); // Automatically mapped!

Mapping Approaches

OmniMapper supports three mapping strategies to fit your architecture:

Approach A: Abstract Class (Centralized Mapper)

@OmniMapper()
abstract class UserMapper {
  UserEntity toEntity(UserModel model);
}
// Generates: class UserMapperImpl extends UserMapper { ... }

Approach B: Extension TO Target

@OmniMapper(target: UserEntity)
class UserModel { ... }
// Generates: extension on UserModel { UserEntity toEntity() { ... } }

Approach C: Extension FROM Source

@OmniMapper(from: UserEntity, methodName: 'toModel')
class UserModel { ... }
// Generates: extension on UserEntity { UserModel toModel() { ... } }

Multiple Mappings

Map a single class to multiple targets using @OmniMappers:

@OmniMappers([
  OmniMapper(target: UserEntity),
  OmniMapper(from: UserEntity, methodName: 'toModel'),
])
class UserModel { ... }

Advanced Features

Custom Field Mapping

When source and target have different property names, you have two options depending on your control over the classes:

Place @OmniField directly on the property. This is the most ergonomic approach because the mapping rule stays right next to the variable declaration.

@OmniMapper(target: UserEntity)
class UserModel {
  @OmniField(name: 'id') // Maps 'userId' to 'id'
  final int userId;
  // ...
}

If you cannot modify the class (e.g., it belongs to a third-party package or is generated code), or if you need advanced custom expressions, use MappingRule inside @OmniMapper.

@OmniMapper(
  target: UserEntity,
  mappings: [
    MappingRule('id', source: 'userId'), // source.userId → target.id
  ],
)
class UserModel {
  final int userId;
  // ...
}

Default Values

Provide fallback values for target fields missing in the source:

Option 1: @OmniField

class UserModel {
  @OmniField(defaultValue: '"active"')
  final String status;
}

Option 2: mappings

@OmniMapper(
  target: UserEntity,
  mappings: [
    MappingRule('status', defaultValue: '"active"'),
    MappingRule('createdAt', defaultValue: 'DateTime.now()'),
  ],
)

Custom Type Converters

Handle type mismatches with OmniConverter:

class DateTimeStringConverter extends OmniConverter<String, DateTime> {
  const DateTimeStringConverter();

  @override
  DateTime convert(String source) => DateTime.parse(source);
}

@OmniMapper(
  target: UserEntity,
  converters: [DateTimeStringConverter],
)
class UserModel {
  final String createdAt; // String → DateTime automatically
}

List Generation

Automatically generates an extension on Iterable<Source> for batch mapping:

final models = [model1, model2, model3];
final entities = models.toEntityList(); // Returns List<UserEntity>

Enabled by default. Disable with generateListMapper: false.

In-Place Updates

Generates a method to update an existing target instance without creating a new one:

final existingEntity = UserEntity(id: 1, name: 'Old');
formModel.updateUserEntity(existingEntity);
// existingEntity.name is now updated — same object in memory!

Disabled by default. Enable with generateUpdateMethod: true. Works with mutable fields only (non-final).

Ignoring Fields

Skip specific fields during mapping:

Option 1: @OmniField

class UserModel {
  @OmniField(ignore: true)
  final String passwordHash;
}

Option 2: mappings

@OmniMapper(
  target: UserEntity,
  mappings: [
    MappingRule('passwordHash', ignore: true),
  ],
)

Polymorphic Mapping (@SubclassMapping)

Handle subclasses and inheritance dynamically based on the runtime type of the source object:

@OmniMapper(
  target: BaseEntity,
  subclasses: [
    SubclassMapping(source: AdminModel, target: AdminEntity),
    SubclassMapping(source: GuestModel, target: GuestEntity),
  ]
)
class BaseModel { ... }

Dependency Injection & Composition (uses)

Reference existing mappers to handle complex nested fields automatically. The generated implementation will ask for these mappers in its constructor:

@OmniMapper()
abstract class AddressMapper {
  AddressEntity toEntity(AddressModel model);
}

@OmniMapper(uses: [AddressMapper])
abstract class UserMapper {
  final AddressMapper addressMapper;
  UserMapper(this.addressMapper);

  UserEntity toEntity(UserModel model);
}

Custom Field Expressions (MappingRule.custom)

Use pure Dart code for extreme flexibility when mapping fields:

@OmniMapper(
  target: UserEntity,
  mappings: [
    MappingRule('fullName', custom: 'firstName + " " + lastName'),
  ],
)
class UserModel { ... }

Multiple Sources Mapping

Combine multiple source objects into a single target object by defining a centralized mapper:

@OmniMapper()
abstract class UserProfileMapper {
  UserProfile toProfile(User user, Address address);
}

Automatic Enum Mapping

Enums are mapped automatically if their values match by name. No additional configuration is required.

Deep Auto-Flattening

Nested objects are automatically flattened if the target fields match the nested structure (e.g., source.user.address.street maps to target.street).

Strict Mode & IgnoreIfNull

Enforce complete mappings and support PATCH-like updates:

@OmniMapper(
  target: UserEntity,
  strictMode: true, // Throws an error if any target field is unmapped
  ignoreIfNull: true, // Null source fields won't overwrite target fields (useful for updates)
)
class UserModel { ... }

To suppress lint warnings on generated files, add this to your project's build.yaml:

targets:
  $default:
    builders:
      source_gen|combining_builder:
        options:
          ignore_for_file:
            - type=lint
            - coverage:ignore-file

Running the Generator

# One-time build
dart run build_runner build -d

# Watch mode (rebuilds on file changes)
dart run build_runner watch -d

Contributing

Contributions are welcome! Please file issues and pull requests on the GitHub repository.

License

This project is licensed under the BSD 3-Clause License — see the LICENSE file for details.

Libraries

omni_mapper