Subclass Mapping
Dispatch mapping to the correct concrete subtype at runtime using @SubclassMapping.
Subclass Mapping
@SubclassMapping lets you map a polymorphic source hierarchy to a matching target hierarchy. The generator produces a Dart 3 switch expression that dispatches to the appropriate sub-mapper based on the runtime type of the source.
Setup
Define your source and target hierarchies, then annotate the dispatch method:
// Source hierarchy (open class — not sealed)
class MediaContent { const MediaContent(); }
class Article extends MediaContent {
final String title;
final String body;
const Article({required this.title, required this.body});
}
class Video extends MediaContent {
final String title;
final int durationSeconds;
const Video({required this.title, required this.durationSeconds});
}
// Target hierarchy
class MediaContentDto { const MediaContentDto(); }
class ArticleDto extends MediaContentDto {
final String title;
final String body;
const ArticleDto({required this.title, required this.body});
}
class VideoDto extends MediaContentDto {
final String title;
final int durationSeconds;
const VideoDto({required this.title, required this.durationSeconds});
}
@Mapper()
abstract class MediaMapper {
@SubclassMapping(source: Article, target: ArticleDto)
@SubclassMapping(source: Video, target: VideoDto)
MediaContentDto toDto(MediaContent source);
// Delegate methods — generated dispatch calls these
ArticleDto toArticleDto(Article source);
VideoDto toVideoDto(Video source);
}
Generated Code
For a non-sealed base class, the generator emits a wildcard fallthrough:
@override
MediaContentDto toDto(MediaContent source) {
return switch (source) {
Article article => toArticleDto(article),
Video video => toVideoDto(video),
_ => throw ArgumentError('Unexpected subtype: ${source.runtimeType}'),
};
}
Sealed Classes
When the base class is sealed, no wildcard is needed — Dart's exhaustiveness check handles it:
sealed class Shape {}
class Circle extends Shape { final double radius; Circle(this.radius); }
class Rect extends Shape { final double w, h; Rect(this.w, this.h); }
sealed class ShapeDto {}
class CircleDto extends ShapeDto { final double radius; CircleDto(this.radius); }
class RectDto extends ShapeDto { final double w, h; RectDto(this.w, this.h); }
@Mapper()
abstract class ShapeMapper {
@SubclassMapping(source: Circle, target: CircleDto)
@SubclassMapping(source: Rect, target: RectDto)
ShapeDto toDto(Shape shape);
CircleDto toCircleDto(Circle source);
RectDto toRectDto(Rect source);
}
Generated:
@override
ShapeDto toDto(Shape shape) {
return switch (shape) {
Circle() => toCircleDto(shape),
Rect() => toRectDto(shape),
};
}
Customizing Subclass Mapping
Apply @Mapping on the delegate methods to control field-level behavior for each subtype:
@Mapper()
abstract class MediaMapper {
@SubclassMapping(source: Article, target: ArticleDto)
@SubclassMapping(source: Video, target: VideoDto)
MediaContentDto toDto(MediaContent source);
@Mapping(target: 'summary', source: 'body')
ArticleDto toArticleDto(Article source);
@Mapping(target: 'lengthSeconds', source: 'durationSeconds')
VideoDto toVideoDto(Video source);
}