Skip to main content

Migrating to AutoMapper 8

Overview

In AutoMapper 8, the overall APIs of AutoMapper have changed from Fluent API to a more Functional approach. AutoMapper 8 also adopts the term Mapping Strategy (Strategy) to replace Mapping Plugin (Plugin). Strategy API is drastically simplified and made consistent across all Strategies.

// before: Plugin Initializer sometimes is a function, other times is a function that needs to be invoked
const mapperOptions = {
// pluginInitializer: classes
// pluginInitializer: pojos
// pluginInitializer: mikro()
// pluginInitializer: sequelize()
};

// after: All Strategies are functions that need to be invoked
const mapperOptions = {
// strategyInitializer: classes()
// strategyInitializer: pojos()
// strategyInitializer: mikro()
// strategyInitializer: sequelize()
};

Migrations

skipLibCheck

If you haven't had skipLibCheck: true in your tsconfig.json already, go ahead and do so.

createMapper(CreateMapperOptions)

  1. CreateMapperOptions no longer requires a name
  2. CreateMapperOptions accepts strategyInitializer instead of pluginInitializer
// before
const mapper = createMapper({
name: 'arbitrary',
pluginInitializer: classes,
/* namingConventions and errorHandler stay the same */
});

// after
const mapper = createMapper({
strategyInitializer: classes(),
/* namingConventions and errorHandler stay the same */
});

createMap()

Previously, createMap() was a method available on the Mapper object (returned by createMapper()). Now, createMap() is a standalone function instead.

Syntax

// before
mapper.createMap(User, UserDto);

// after
createMap(mapper, User, UserDto);

API

Previously, mapper.createMap() returns a CreateMapFluentFunctions which allows you to chain forMember() and other methods to customize the Mapping you are creating. Now, createMap() returns a Mapping itself. Customization is provided by using other standalone functions. In AutoMapper 8, we call these functions MappingConfiguration.

// before
mapper
.createMap(User, UserDto)
.forMember(/* ... */)
.forSelf(/* ... */)
.beforeMap(/* ... */)
.afterMap(/* ... */);

// after
createMap(
mapper,
User,
UserDto,
forMember(/* ... */),
forSelf(/* ... */),
beforeMap(/* ... */),
afterMap(/* ... */)
);

This Functional API allows for better tree-shaking and grouping MappingConfiguration into reusable functions that you can use on different createMap().

CreateMapOptions

Previously, you can pass in a CreateMapOptions to mapper.createMap(). Now, those options are provided by different standalone MappingConfiguration as well.

// before
mapper.createMap(Base, BaseDto);
mapper.createMap(User, UserDto, {
extend: [mapper.getMapping(Base, BaseDto)],
namingConventions: new PascalCaseNamingConvention(),
});

// after
const baseMapping = createMap(mapper, Base, BaseDto);
createMap(
mapper,
User,
UserDto,
extend(baseMapping),
// or you can call: extend(Base, BaseDto)
namingConventions(new PascalCaseNamingConvention())
);

addProfile()

Same as createMap(), addProfile() was a method on the Mapper in previous versions. Now, addProfile() is a standalone function

Syntax

// before
mapper.addProfile(productProfile).addProfile(userProfile);

// after
addProfile(mapper, productProfile);
addProfile(mapper, userProfile);

API

Previously, mapper.addProfile() returns the Mapper so you can chain addProfile(). Now, addProfile() is a void function. The main difference is addProfile() now accepts a list of MappingConfiguration as well. The idea is you can have common MappingConfiguration that you can pass to ALL the createMap() inside of a particular MappingProfile.

// before
const productProfile: MappingProfile = (mapper) => {
const baseMapping = mapper.getMapping(Base, BaseDto);
const camelCaseConvention = new CamelCaseNamingConvention();

const beforeMap = () => {
/* do something common for all Mappings */
};
const afterMap = () => {
/* do something common for all Mappings */
};

// duplicate the configurations for all Mappings
mapper
.createMap(Product, ProductDto, {
extend: [baseMapping],
namingConventions: camelCaseConvention,
})
.beforeMap(beforeMap)
.afterMap(afterMap);

mapper
.createMap(Product, ProductDetailDto, {
extend: [baseMapping],
namingConventions: camelCaseConvention,
})
.beforeMap(beforeMap)
.afterMap(afterMap);

mapper
.createMap(Product, MinimalProductDto, {
extend: [baseMapping],
namingConventions: camelCaseConvention,
})
.beforeMap(beforeMap)
.afterMap(afterMap);
};
mapper.addProfile(productProfile);

// after
const productProfile: MappingProfile = (mapper) => {
createMap(mapper, Product, ProductDto);
createMap(mapper, Product, ProductDetailDto);
createMap(mapper, Product, MinimalProductDto);
};

// pass the common configurations to the profile
addProfile(
mapper,
productProfile,
extend(Base, BaseDto),
namingConventions(new CamelCaseNamingConvention()),
beforeMap(() => {
/* do something common for all Mappings */
}),
afterMap(() => {
/* do something common for all Mappings */
})
);

AutoMapper 8 version is cleaner. How it works is each MappingProfile has a MappingProfileContext created upon invoked. All the createMap() inside a particular MappingProfile has access to that MappingProfileContext which includes all the common MappingConfiguration.

addTypeConverter()

Previously, addTypeConverter() is a method on the Mapper object that allows you to create a converter between two types. The type converter then gets applied to every pair of properties that match the two types. However, type converters added by addTypeConverter will be applied for ALL mappings, and that might not be the case.

In AutoMapper 8, typeConverter() is a MappingConfiguration function that can be passed to createMap() (to configure for that Mapping) or addProfile() (to configure for all mappings inside a MappingProfile). There is no equivalence to Mapper level type converters in AutoMapper 8.

// before
mapper.addTypeConverter(String, Number, (str) => parseInt(str));

mapper.createMap(User, UserDto);
mapper.createMap(Product, ProductDto);

// after
createMap(
mapper,
User,
UserDto,
typeConverter(String, Number, (str) => parseInt(str))
);
createMap(
mapper,
Product,
ProductDto,
typeConverter(String, Number, (str) => parseInt(str) + 10)
);

map()

Syntax

// before
mapper.map(user, UserDto, User);

// after
mapper.map(user, User, UserDto);

This is a subtle breaking change that I've been holding off for a while. The positional arguments for Source and Destination are now switched to be more logical: "I want to map sourceObject from Source to Destination". This particularly makes it less confusing for @automapper/pojos users.

// before
mapper.map<User, UserDto>(user, 'UserDto', 'User'); // weird order

// after
mapper.map<User, UserDto>(user, 'User', 'UserDto'); // matching order

API

Mutation

The mutation API of mapper.map() has been separated into a whole set of methods on Mapper, mapper.mutate(). This is to create a clear distinction between mapping and mutating.

// before
const destinationObj = {};
mapper.map(sourceObj, Destination, Source, destinationObj);

// after
const destinationObj = {};
mapper.mutate(sourceObj, destinationObj, Source, Destination);

The mapper.mutate() family has matching APIs with mapper.map()

mapper.map();
mapper.mapAsync();
mapper.mapArray();
mapper.mapArrayAsync();

mapper.mutate();
mapper.mutateAsync();
mapper.mutateArray();
mapper.mutateArrayAsync();
Extra Arguments

When invoking a map operation with mapper.map(), you have the ability to pass in a MapOptions object with a property called: extraArguments. This is to allow for passing in dynamic arguments (that are only available at the time the map is invoked) to a particular map operation.

In AutoMapper 8, extraArguments has been renamed to extraArgs and is a Function instead of just a Record<string, unknown>.

// before
mapper.map(user, UserDto, User, { extraArguments: { extra: 123 } });

// after
mapper.map(user, User, UserDto, {
extraArgs: (mapping, destinationObject) => ({ extra: 123 }),
});

With extraArgs being a function, you will have all the information you need to return a more sophisticated extra arguments object. In addition to the Mapper and the sourceObject that you already have access to (eg: mapper.map(sourceObject, ...)), extraArgs is called with the Mapping and the destinationObject, which you don't easily have access to.

caution

destinationObject might not be the complete destination object because it might be a particular property's turn with mapWithArguments.

MemberMapFunctions

convertUsing

The 2nd argument (Selector) is now required.

// before
const dateToStringConverter: Converter<User, string> = {
convert(source: User): string {
return source.birthday.toDateString();
},
};
mapper.createMap(User, UserDto).forMember(
(d) => d.birthday,
// 2nd argument is optional.
// If not passed in, convertUsing will call the converter#convert with the whole sourceObject
convertUsing(dateToStringConverter)
);

// after
const dateToStringConverter: Converter<Date, string> = {
convert(source: Date): string {
return source.toDateString();
},
};
createMap(
mapper,
User,
UserDto,
forMember(
(d) => d.birthday,
// 2nd argument is required.
convertUsing(dateToStringConverter, (src) => src.birthday)
)
);

This breaking change encourages better Converter usages. In the above example, it makes dateToStringConverter easier to reuse. If you have use-cases that use the whole sourceObject, consider using mapFrom(Resolver) instead.

Strategy (previously Plugin)

Strategy can be customized with MappingStrategyInitializerOptions which has the following interface:

export interface MappingStrategyInitializerOptions {
applyMetadata?: ApplyMetadata;
destinationConstructor?: DestinationConstructor;
preMap?<TSource extends Dictionary<TSource>>(source: TSource): TSource;
postMap?<
TSource extends Dictionary<TSource>,
TDestination extends Dictionary<TDestination>
>(
source: TSource,
destination: TDestination
): TDestination;
}

const mapper = createMapper({
strategyInitializer: classes({
applyMetadata, // customize how a Strategy applies the metadata to a model
destinationConstructor, // customize the default constructor of the Destination model
preMap, // customize what to do before a map happens
postMap, // customize what to do after a map happens
}),
});

@automapper/classes

Initializer

// before
const mapper = createMapper({
pluginInitializer: classes, // no ()
});

// after
const mapper = createMapper({
strategyInitializer: classes(), // invoking
});

AutoMap

  • typeFn has been renamed to type
// before
@AutoMap({ typeFn: () => User })

// after
@AutoMap({ type: () => User })
  • If you only have typeFn in AutoMapOptions, you can omit the object altogether
// before
@AutoMap({ typeFn: () => User })

// after
@AutoMap(() => User)
  • Default depth is set to 1 instead of 0.
  • AutoMap now warns if it cannot infer the type of the property. If you have dynamic type like any, Record, or something similar, please configure the property via forMember().
  • Array metadata is now required to be explicitly specified
// before
export class User {
@AutoMap({ typeFn: () => Address })
addresses: Address[];
}

// after
export class User {
@AutoMap(() => [Address])
addresses: Address[];
}

@automapper/pojos

Initializer

// before
const mapper = createMapper({
pluginInitializer: pojos, // no ()
});

// after
const mapper = createMapper({
strategyInitializer: pojos(), // invoking
});

createMetadataMap()

  • Has been replaced with PojosMetadataMap.create()
    • Accepts string | symbol for the key instead of just string
    • No longer accepts null for metadata.
    • No longer allows for "extending" previously created metadata. Explicit is better in the case of metadata.
  • PojosMetadataMap.reset() has been added to clear all the stored metadata (useful for the testing environment)
// before
createMetadataMap('SimpleUser', {
firstName: String,
lastName: String,
});
createMetadataMap('SimpleUserDto', 'SimpleUser', {
fullName: String,
});

// after
PojosMetadataMap.create<SimpleUser>('SimpleUser', {
firstName: String,
lastName: String,
});
PojosMetadataMap.create<SimpleUserDto>('SimpleUserDto', {
firstName: String,
lastName: String,
fullName: String,
});

If you want to share firstName and lastName, you can always make an object and spread it.

  • Array metadata is now required to be explicitly specified
export interface User {
addresses: Address[];
}

// before
createMetadataMap<User>('User', {
addresses: 'Address',
});

// after
PojosMetadataMap.create<User>('User', {
addresses: ['Address'],
});

NestJS

AutomapperModule.forRoot

// before
AutomapperModule.forRoot({
singular: true,
options: [
{
pluginInitializer: classes,
namingConventions: new CamelCaseNamingConvention(),
},
],
globalErrorHandler,
globalNamingConventions,
});

AutomapperModule.forRoot({
options: [
{
name: 'classes',
pluginInitializer: classes,
},
{
name: 'pojos',
pluginInitializer: pojos,
},
],
globalErrorHandler,
globalNamingConventions: new CamelCaseNamingConvention(),
});

// after
AutomapperModule.forRoot({
strategyInitializer: classes(),
namingConventions: new CamelCaseNamingConvention(),
});
AutomapperModule.forRoot(
[
{
name: 'classes',
strategyInitializer: classes(),
},
{
name: 'pojos',
strategyInitializer: pojos(),
},
],
{
globalErrorHandler,
globalNamingConventions: new CamelCaseNamingConvention(),
}
);

AutomapperModule.forRootAsync

import { EntityManager } from '@mikro-orm/mongodb';

AutomapperModule.forRootAsync({
inject: [EntityManager],
useFactory: (em: EntityManager) => ({
strategyInitializer: classes({
destinationConstructor: (sourceObject, destinationIdentifier) =>
em.create(destinationIdentifier, {}),
}),
}),
});

AutomapperProfile

  • mapProfile has been changed to get profile
@Injectable()
export class UserProfile extends AutomapperProfile {
constructor(@InjectMapper() mapper: Mapper) {
super(mapper);
}

// before
override mapProfile(): MappingProfile {
return (mapper) => {
mapper.createMap(/*...*/);
};
}

// after
override get profile(): MappingProfile {
return (mapper) => {
createMap(mapper /*...*/);
};
}
}
  • A new getter get mappingConfigurations that returns a MappingConfiguration[] to be passed to all createMap() inside the Profile
@Injectable()
export class UserProfile extends AutomapperProfile {
constructor(@InjectMapper() mapper: Mapper) {
super(mapper);
}

get profile(): MappingProfile {
return (mapper) => {
createMap(mapper, UserEntity, UserDto);
createMap(mapper, UserEntity, UserInformationDto);
createMap(mapper, UserEntity, AuthUserDto);
};
}

protected get mappingConfigurations(): MappingConfiguration[] {
// the 3 createMap() above will get this `extend()`
return [extend(BaseEntity, BaseDto)];
}
}

MapPipe

export class SomeController {
// before
@Post()
someMethod(@Body(MapPipe(UserDto, User)) dto: UserDto) {
/*..*/
}

// after
@Post()
someMethod(@Body(MapPipe(User, UserDto)) dto: UserDto) {
/*..*/
}
}

MapInterceptor

export class SomeController {
// before
@Get()
@UseInterceptors(MapInterceptor(UserDto, User))
get() {
/*...*/
}

// after
@Get()
@UseInterceptors(MapInterceptor(User, UserDto))
get() {
/*...*/
}
}