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., Model → Entity, DTO → ViewModel), 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