I've started to play with NestJS, migrating from my old express/mongoose project and immediately crashed into a fence, just following MongoDB/serializations chapters from NestJS docs. I've prepared following schema
/////// schema
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import * as mongoose from 'mongoose';
import { Exclude, Expose } from 'class-transformer';
export type UserDocument = User & mongoose.Document;
@Schema()
export class User {
    @Prop()
    @Exclude()
    _id: String
    @Expose()
    get id(): String { return this._id ? `${this._id}` : undefined }
    @Prop()
    name: string
    @Prop({ unique: true })
    login: string
    @Exclude()
    @Prop()
    password: string        
}
export const UserSchema = SchemaFactory.createForClass(User);
registered it in app.module
MongooseModule.forRoot('mongodb://localhost/old_project'), 
MongooseModule.forFeature([ { name: User.name, schema: UserSchema } ]),
and tried following calls, expecting no password property revealed in results
/////// controller
  @UseInterceptors(ClassSerializerInterceptor)
  @Get('default')
  async default(): Promise<User> {
    let u = new User();
    u.name = 'Kos';
    u.password = "secret";
    u.login = 'k@o.s'
    return u;
  }
  
  // returns
  // {"name":"Kos","login":"k@o.s"}
  @Get('first_raw')
  async firstRaw(): Promise<User> {
    return this.userModel.findOne()
  }
  
  @Get('first_lean')
  async firstLean(): Promise<User> {
    return this.userModel.findOne().lean()
  }
  
  //both return
  // {"_id":"5f8731a36fc003421db08921","name":"Kos","login":"kos","password":"secret","__v":0}
  @UseInterceptors(ClassSerializerInterceptor)
  @Get('first_raw_stripped')
  async firstRawStripped(): Promise<User> {
    return this.userModel.findOne()
  }
  
  //returns
  // {"$__":{"strictMode":true,"selected":{},"getters":{},"_id":"5f8731a36fc003421db08921","wasPopulated":false,"activePaths":{"paths":{"_id":"init","name":"init","login":"init","password":"init","__v":"init"},"states":{"ignore":{},"default":{},"init":{"_id":true,"name":true,"login":true,"password":true,"__v":true},"modify":{},"require":{}},"stateNames":["require","modify","init","default","ignore"]},"pathsToScopes":{},"cachedRequired":{},"$setCalled":[],"emitter":{"_events":{},"_eventsCount":0,"_maxListeners":0},"$options":{"skipId":true,"isNew":false,"willInit":true,"defaults":true}},"isNew":false,"$locals":{},"$op":null,"_doc":{"_id":"5f8731a36fc003421db08921","name":"Kos","login":"kos","password":"secret","__v":0},"$init":true}
  @UseInterceptors(ClassSerializerInterceptor)
  @Get('first_lean_stripped')
  async firstLeanStripped(): Promise<User> {
    return this.userModel.findOne().lean()
  }
  
  //returns
  // {"_id":"5f8731a36fc003421db08921","name":"Kos","login":"kos","password":"secret","__v":0}
Finally I've found that only manual instantiation of User class does somehow what it should do, so I've added constructor to User class
constructor(partial?: Partial<User>) {
    if (partial)
        Object.assign(this, partial);
}
and then it finally returned what was expected - no password prop in result
  @UseInterceptors(ClassSerializerInterceptor)
  @Get('first')
  async first(): Promise<User> {
    return new User(await this.userModel.findOne().lean());
  }
  
  //finally returns what's expected
  // {"name":"Kos","login":"kos","__v":0,"id":"5f8731a36fc003421db08921"}
Am I missing something? Somehow it seems a bit overwhelming...
UPDATE: it is either question about NestJS mongoose and serialization coupling - why this
  @UseInterceptors(ClassSerializerInterceptor)
  @Get('first')
  async first(): Promise<User> {
    return await this.userModel.findOne().lean();
  }
doesn't work and this
  @UseInterceptors(ClassSerializerInterceptor)
  @Get('first')
  async first(): Promise<User> {
    return new User(await this.userModel.findOne().lean());
  }
works (which also means for each result enumerable map with entity creations required)