Repository: typestack/class-transformer Branch: develop Commit: a2734a5d154c Files: 96 Total size: 279.1 KB Directory structure: gitextract_zd9snzjs/ ├── .eslintrc.yml ├── .gitbook.yaml ├── .github/ │ ├── dependabot.yml │ ├── semantic.yml │ └── workflows/ │ ├── auto-approve-dependabot-workflow.yml │ ├── continuous-deployment-workflow.yml │ ├── continuous-integration-workflow.yml │ └── lock-closed-issues-workflow.yml ├── .gitignore ├── .prettierrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── codecov.yml ├── docs/ │ ├── SUMMARY.md │ └── pages/ │ ├── 01-getting-started.md │ └── 02-basic-usage.md ├── jest.config.js ├── package.json ├── rollup.config.js ├── sample/ │ ├── sample1-simple-usage/ │ │ ├── Album.ts │ │ ├── Photo.ts │ │ ├── User.ts │ │ └── app.ts │ ├── sample2-iheritance/ │ │ ├── Album.ts │ │ ├── Authorable.ts │ │ ├── Photo.ts │ │ ├── User.ts │ │ └── app.ts │ ├── sample3-custom-arrays/ │ │ ├── Album.ts │ │ ├── AlbumArray.ts │ │ ├── Photo.ts │ │ └── app.ts │ ├── sample4-generics/ │ │ ├── SimpleCollection.ts │ │ ├── SuperCollection.ts │ │ ├── User.ts │ │ └── app.ts │ └── sample5-custom-transformer/ │ ├── User.ts │ └── app.ts ├── src/ │ ├── ClassTransformer.ts │ ├── MetadataStorage.ts │ ├── TransformOperationExecutor.ts │ ├── constants/ │ │ └── default-options.constant.ts │ ├── decorators/ │ │ ├── exclude.decorator.ts │ │ ├── expose.decorator.ts │ │ ├── index.ts │ │ ├── transform-instance-to-instance.decorator.ts │ │ ├── transform-instance-to-plain.decorator.ts │ │ ├── transform-plain-to-instance.decorator.ts │ │ ├── transform.decorator.ts │ │ └── type.decorator.ts │ ├── enums/ │ │ ├── index.ts │ │ └── transformation-type.enum.ts │ ├── index.ts │ ├── interfaces/ │ │ ├── class-constructor.type.ts │ │ ├── class-transformer-options.interface.ts │ │ ├── decorator-options/ │ │ │ ├── exclude-options.interface.ts │ │ │ ├── expose-options.interface.ts │ │ │ ├── transform-options.interface.ts │ │ │ ├── type-discriminator-descriptor.interface.ts │ │ │ └── type-options.interface.ts │ │ ├── index.ts │ │ ├── metadata/ │ │ │ ├── exclude-metadata.interface.ts │ │ │ ├── expose-metadata.interface.ts │ │ │ ├── transform-fn-params.interface.ts │ │ │ ├── transform-metadata.interface.ts │ │ │ └── type-metadata.interface.ts │ │ ├── target-map.interface.ts │ │ └── type-help-options.interface.ts │ ├── storage.ts │ └── utils/ │ ├── get-global.util.spect.ts │ ├── get-global.util.ts │ ├── index.ts │ └── is-promise.util.ts ├── test/ │ └── functional/ │ ├── basic-functionality.spec.ts │ ├── circular-reference-problem.spec.ts │ ├── custom-transform.spec.ts │ ├── default-values.spec.ts │ ├── es6-data-types.spec.ts │ ├── ignore-decorators.spec.ts │ ├── implicit-type-declarations.spec.ts │ ├── inheritence.spec.ts │ ├── prevent-array-bomb.spec.ts │ ├── promise-field.spec.ts │ ├── serialization-deserialization.spec.ts │ ├── specify-maps.spec.ts │ ├── transformation-option.spec.ts │ ├── transformer-method.spec.ts │ └── transformer-order.spec.ts ├── tsconfig.json ├── tsconfig.prod.cjs.json ├── tsconfig.prod.esm2015.json ├── tsconfig.prod.esm5.json ├── tsconfig.prod.json ├── tsconfig.prod.types.json └── tsconfig.spec.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.yml ================================================ parser: '@typescript-eslint/parser' plugins: - '@typescript-eslint' parserOptions: ecmaVersion: 2018 sourceType: module project: - ./tsconfig.json - ./tsconfig.spec.json extends: - 'plugin:@typescript-eslint/recommended' - 'plugin:@typescript-eslint/recommended-requiring-type-checking' - 'plugin:jest/recommended' - 'prettier' rules: '@typescript-eslint/explicit-member-accessibility': off '@typescript-eslint/no-angle-bracket-type-assertion': off '@typescript-eslint/no-parameter-properties': off '@typescript-eslint/explicit-function-return-type': off '@typescript-eslint/member-delimiter-style': off '@typescript-eslint/no-inferrable-types': off '@typescript-eslint/no-explicit-any': off '@typescript-eslint/member-ordering': 'error' '@typescript-eslint/no-unused-vars': - 'error' - args: 'none' # TODO: Remove these and fixed issues once we merged all the current PRs. '@typescript-eslint/ban-types': off '@typescript-eslint/no-unsafe-return': off '@typescript-eslint/no-unsafe-assignment': off '@typescript-eslint/no-unsafe-call': off '@typescript-eslint/no-unsafe-member-access': off '@typescript-eslint/explicit-module-boundary-types': off ================================================ FILE: .gitbook.yaml ================================================ root: ./docs ​structure: readme: pages/01-getting-started.md summary: SUMMARY.md​ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily time: "10:00" timezone: Europe/Budapest open-pull-requests-limit: 0 versioning-strategy: increase commit-message: prefix: build include: scope ignore: - dependency-name: "husky" ================================================ FILE: .github/semantic.yml ================================================ titleAndCommits: true allowMergeCommits: false scopes: - deps - deps-dev types: - feat - fix - docs - style - refactor - perf - test - build - ci - chore - revert - merge ================================================ FILE: .github/workflows/auto-approve-dependabot-workflow.yml ================================================ name: Dependabot auto-merge on: pull_request_target jobs: dependabot: runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' steps: - name: 'Auto approve PR by Dependabot' uses: hmarr/auto-approve-action@v2.0.0 with: github-token: "${{ secrets.TYPESTACK_BOT_TOKEN }}" - name: 'Comment merge command' uses: actions/github-script@v3 with: github-token: ${{secrets.TYPESTACK_BOT_TOKEN }} script: | await github.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: '@dependabot squash and merge' }) ================================================ FILE: .github/workflows/continuous-deployment-workflow.yml ================================================ name: CD on: release: types: [created] jobs: publish: name: Publish to NPM runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: registry-url: https://registry.npmjs.org - run: npm ci --ignore-scripts - run: npm run prettier:check - run: npm run lint:check - run: npm run test:ci - run: npm run build:es2015 - run: npm run build:esm5 - run: npm run build:cjs - run: npm run build:umd - run: npm run build:types - run: cp LICENSE build/LICENSE - run: cp README.md build/README.md - run: jq 'del(.devDependencies) | del(.scripts)' package.json > build/package.json - run: npm publish ./build env: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} ================================================ FILE: .github/workflows/continuous-integration-workflow.yml ================================================ name: CI on: [push, pull_request] jobs: checks: name: Linters runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 - run: npm ci --ignore-scripts - run: npm run prettier:check - run: npm run lint:check tests: name: Tests runs-on: ubuntu-latest strategy: matrix: node-version: ['10.x', '12.x', '14.x'] fail-fast: false steps: - uses: actions/checkout@v1 - name: Setting up Node.js (v${{ matrix.node-version }}.x) uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm ci --ignore-scripts - run: npm run test:ci - run: npm install codecov -g if: ${{ matrix.node-version == '14.x' }} - run: codecov -f ./coverage/clover.xml -t ${{ secrets.CODECOV_TOKEN }} --commit=$GITHUB_SHA --branch=${GITHUB_REF##*/} if: ${{ matrix.node-version == '14.x' }} build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 - run: npm ci --ignore-scripts - run: npm run build:es2015 - run: npm run build:esm5 - run: npm run build:cjs - run: npm run build:umd - run: npm run build:types ================================================ FILE: .github/workflows/lock-closed-issues-workflow.yml ================================================ name: 'Lock inactive threads' on: schedule: - cron: '0 0 * * *' jobs: lock: name: Lock closed issues runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v2 with: github-token: ${{ github.token }} issue-lock-inactive-days: 30 pr-lock-inactive-days: 30 issue-lock-comment: > This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. pr-lock-comment: > This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. ================================================ FILE: .gitignore ================================================ # Log files logs *.log *.tmp *.tmp.* log.txt npm-debug.log* # Testing output lib-cov/** coverage/** # Environment files .env # Dependency directories node_modules # MacOS related files *.DS_Store .AppleDouble .LSOverride ._* UserInterfaceState.xcuserstate # Windows related files Thumbs.db Desktop.ini $RECYCLE.BIN/ # IDE - Sublime *.sublime-project *.sublime-workspace # IDE - VSCode .vscode/** !.vscode/tasks.json !.vscode/launch.json # IDE - IntelliJ .idea # Compilation output folders dist/ build/ tmp/ out-tsc/ temp # Files for playing around locally playground.ts playground.js ================================================ FILE: .prettierrc.yml ================================================ printWidth: 120 tabWidth: 2 useTabs: false semi: true singleQuote: true trailingComma: es5 bracketSpacing: true arrowParens: avoid ================================================ FILE: CHANGELOG.md ================================================ # Changelog _This changelog follows the [keep a changelog][keep-a-changelog]_ format to maintain a human readable changelog. ### [0.5.1][v0.5.1] [BREAKING CHANGE] - 2021-11-22 #### Changed - re-added accidentally removed deprecated function names `classToPlain` and `plainToClass` ### [0.5.0][v0.5.0] [BREAKING CHANGE] - 2021-11-20 > **NOTE:** This version fixes a security vulnerability allowing denial of service attacks with a specially crafted request payload. Please update as soon as possible. #### Breaking Changes See the breaking changes from `0.4.1` release. It was accidentally released as patch version. ### [0.4.1][v0.4.1] [BREAKING CHANGE] - 2021-11-20 > **NOTE:** This version fixes a security vulnerability allowing denial of service attacks with a specially crafted request payload. Please update as soon as possible. #### Breaking Changes **Exported functions has been renamed** Some of the exported functions has been renamed to better reflect what they are doing. - `classToPlain` -> `instanceToPlain` - `plainToClass` -> `plainToInstance` - `classToClass` -> `instanceToInstance` #### Fixed - prevent unhandled error in `plaintToclass` when union-type member is undefined - fixed a scenario when a specially crafted JS object would be parsed to Array #### Changed - various dev-dependencies updated ### [0.4.0][v0.4.0] [BREAKING CHANGE] - 2021-02-14 #### Breaking Changes See the breaking changes from `0.3.2` release. It was accidentally released as patch version. #### Added - add option to ignore unset properties - `group` information is exposed in the `@Transform` handler - transformation options are exposed in the `@Transform` handler #### Fixed - fixed TypeError when `discriminator.subTypes` is not defined #### Changed - various dev-dependencies has been updated ### [0.3.2][v0.3.2] [BREAKING CHANGE] - 2021-01-14 #### Breaking Changes **Signature change for `@Transform` decorator** From this version the `@Transform` decorator receives the transformation parameters in a a wrapper object. You need to destructure the values you are interested in. Old way: ```ts @Transform((value, obj, type) => /* Do some stuff with value here. */) ``` New way with wrapper object: ```ts @Transform(({ value, key, obj, type }) => /* Do some stuff with value here. */) ``` #### Added - `exposeDefaultValues` option has been added, when enabled properties will use their default values when no value is present for the property - the name of the currently transformed parameter is exposed in the `@Transform` decorator #### Fixed - fixed an issue with transforming `Map` (#319) - fixed an issue with sourcemap generation (#472) #### Changed - various internal refactors - various changes to the project tooling - various dev-dependencies has been updated ### [0.3.1][v0.3.1] - 2020-07-29 #### Added - table of content added to readme #### Changed - moved from Mocha to Jest - added Prettier for code formatting - added Eslint for linting - updated CI configuration - removed some unused dev dependencies - updated dependencies to latest version #### Fixed - circular dependency fixed - dev dependencies removed from package.json before publishing (no more security warnings) - transformer order is deterministic now (#231) - fix prototype pollution issue (#367) - various fixes in documentation ### [0.2.3][v0.2.3] [BREAKING CHANGE] #### Changed - `enableImplicitConversion` has been added and imlplicit value conversion is disabled by default. - reverted #234 - fix: write properties with defined default values on prototype which broke the `@Exclude` decorator. ### [0.2.2][v0.2.2] [BREAKING CHANGE] > **NOTE:** This version is deprecated. This version has introduced a breaking-change when this library is used with class-validator. See #257 for details. #### Added - implicity type conversion between values. ### [0.2.1][v0.2.1] > **NOTE:** This version is deprecated. #### Added - add option to strip unkown properties via using the `excludeExtraneousValues` option ### [0.2.0][v0.2.0] [BREAKING CHANGE] #### Added - add documentation for using `Set`s and `Map`s - add opotion to pass a discriminator function to convert values into different types based on custom conditions - added support for polymorphism based on a named type property #### Fixed - fix bug when transforming `null` values as primitives ### 0.1.10 #### Fixed - improve MetadataStorage perf by changing from Arrays to ES6 Maps by @sheiidan - fixed getAncestor issue with unknown nested properties by @247GradLabs ### 0.1.9 #### Fixed - objects with `null` prototype are converted properly now - objects with unknown non primitive properties are converted properly now - corrected a typo in the README.md - fixed the deserialize example in the README.md ### 0.1.4 #### Added - added `TransformClassToPlain` and `TransformClassToClass` decorators ### 0.1.0 #### Added - renamed library from `constructor-utils` to `class-transformer` - completely renamed most of names - renamed all main methods: `plainToConstructor` now is `plainToClass` and `constructorToPlain` is `classToPlain`, etc. - `plainToConstructorArray` method removed - now `plainToClass` handles it - `@Skip()` decorator renamed to `@Exclude()` - added `@Expose` decorator - added lot of new options: groups, versioning, custom names, etc. - methods and getters that should be exposed must be decorated with `@Expose` decorator - added `excludedPrefix` to class transform options that allows exclude properties that start with one of the given prefix ### 0.0.22 #### Fixed - fixed array with primitive types being converted ### 0.0.18-0.0.21 #### Fixed - fixed bugs when getters are not converted with es6 target ### 0.0.17 #### Fixed - fixed issue #4 - added type guessing during transformation from constructor to plain object - added sample with generics ### 0.0.16 #### Changed - renamed `constructor-utils/constructor-utils` to `constructor-utils` package namespace ### 0.0.15 #### Removed - removed code mappings from package ### 0.0.14 #### Removed - removed `import "reflect-metadata"` from source code. Now reflect metadata should be included like any other shims. ### 0.0.13 #### Changed - Library has changed its name from `serializer.ts` to `constructor-utils`. - Added `constructor-utils` namespace. [v0.5.1]: https://github.com/typestack/class-transformer/compare/v0.5.0...v0.5.1 [v0.5.0]: https://github.com/typestack/class-transformer/compare/v0.4.1...v0.5.0 [v0.4.1]: https://github.com/typestack/class-transformer/compare/v0.4.0...v0.4.1 [v0.4.0]: https://github.com/typestack/class-transformer/compare/v0.3.2...v0.4.0 [v0.3.2]: https://github.com/typestack/class-transformer/compare/v0.3.1...v0.3.2 [v0.3.1]: https://github.com/typestack/class-transformer/compare/v0.2.3...v0.3.1 [v0.2.3]: https://github.com/typestack/class-transformer/compare/v0.2.2...v0.2.3 [v0.2.2]: https://github.com/typestack/class-transformer/compare/v0.2.1...v0.2.2 [v0.2.1]: https://github.com/typestack/class-transformer/compare/v0.2.0...v0.2.1 [v0.2.0]: https://github.com/typestack/class-transformer/compare/v0.1.10...v0.2.0 [keep-a-changelog]: https://keepachangelog.com/en/1.0.0/ ================================================ FILE: LICENSE ================================================ The MIT License Copyright (c) 2015-2020 TypeStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # class-transformer ![Build Status](https://github.com/typestack/class-transformer/workflows/CI/badge.svg) [![codecov](https://codecov.io/gh/typestack/class-transformer/branch/develop/graph/badge.svg)](https://codecov.io/gh/typestack/class-transformer) [![npm version](https://badge.fury.io/js/class-transformer.svg)](https://badge.fury.io/js/class-transformer) Its ES6 and Typescript era. Nowadays you are working with classes and constructor objects more than ever. Class-transformer allows you to transform plain object to some instance of class and versa. Also it allows to serialize / deserialize object based on criteria. This tool is super useful on both frontend and backend. Example how to use with angular 2 in [plunker](http://plnkr.co/edit/Mja1ZYAjVySWASMHVB9R). Source code is available [here](https://github.com/pleerock/class-transformer-demo). ## Table of contents - [What is class-transformer](#what-is-class-transformer) - [Installation](#installation) - [Node.js](#nodejs) - [Browser](#browser) - [Methods](#methods) - [plainToInstance](#plaintoinstance) - [plainToClassFromExist](#plaintoclassfromexist) - [instanceToPlain](#instancetoplain) - [instanceToInstance](#instanceToInstance) - [serialize](#serialize) - [deserialize and deserializeArray](#deserialize-and-deserializearray) - [Enforcing type-safe instance](#enforcing-type-safe-instance) - [Working with nested objects](#working-with-nested-objects) - [Providing more than one type option](#providing-more-than-one-type-option) - [Exposing getters and method return values](#exposing-getters-and-method-return-values) - [Exposing properties with different names](#exposing-properties-with-different-names) - [Skipping specific properties](#skipping-specific-properties) - [Skipping depend of operation](#skipping-depend-of-operation) - [Skipping all properties of the class](#skipping-all-properties-of-the-class) - [Skipping private properties, or some prefixed properties](#skipping-private-properties-or-some-prefixed-properties) - [Using groups to control excluded properties](#using-groups-to-control-excluded-properties) - [Using versioning to control exposed and excluded properties](#using-versioning-to-control-exposed-and-excluded-properties) - [Сonverting date strings into Date objects](#сonverting-date-strings-into-date-objects) - [Working with arrays](#working-with-arrays) - [Additional data transformation](#additional-data-transformation) - [Basic usage](#basic-usage) - [Advanced usage](#advanced-usage) - [Other decorators](#other-decorators) - [Working with generics](#working-with-generics) - [Implicit type conversion](#implicit-type-conversion) - [How does it handle circular references?](#how-does-it-handle-circular-references) - [Example with Angular2](#example-with-angular2) - [Samples](#samples) - [Release notes](#release-notes) ## What is class-transformer[⬆](#table-of-contents) In JavaScript there are two types of objects: - plain (literal) objects - class (constructor) objects Plain objects are objects that are instances of `Object` class. Sometimes they are called **literal** objects, when created via `{}` notation. Class objects are instances of classes with own defined constructor, properties and methods. Usually you define them via `class` notation. So, what is the problem? Sometimes you want to transform plain javascript object to the ES6 **classes** you have. For example, if you are loading a json from your backend, some api or from a json file, and after you `JSON.parse` it you have a plain javascript object, not instance of class you have. For example you have a list of users in your `users.json` that you are loading: ```json [ { "id": 1, "firstName": "Johny", "lastName": "Cage", "age": 27 }, { "id": 2, "firstName": "Ismoil", "lastName": "Somoni", "age": 50 }, { "id": 3, "firstName": "Luke", "lastName": "Dacascos", "age": 12 } ] ``` And you have a `User` class: ```typescript export class User { id: number; firstName: string; lastName: string; age: number; getName() { return this.firstName + ' ' + this.lastName; } isAdult() { return this.age > 36 && this.age < 60; } } ``` You are assuming that you are downloading users of type `User` from `users.json` file and may want to write following code: ```typescript fetch('users.json').then((users: User[]) => { // you can use users here, and type hinting also will be available to you, // but users are not actually instances of User class // this means that you can't use methods of User class }); ``` In this code you can use `users[0].id`, you can also use `users[0].firstName` and `users[0].lastName`. However you cannot use `users[0].getName()` or `users[0].isAdult()` because "users" actually is array of plain javascript objects, not instances of User object. You actually lied to compiler when you said that its `users: User[]`. So what to do? How to make a `users` array of instances of `User` objects instead of plain javascript objects? Solution is to create new instances of User object and manually copy all properties to new objects. But things may go wrong very fast once you have a more complex object hierarchy. Alternatives? Yes, you can use class-transformer. Purpose of this library is to help you to map your plain javascript objects to the instances of classes you have. This library also great for models exposed in your APIs, because it provides a great tooling to control what your models are exposing in your API. Here is an example how it will look like: ```typescript fetch('users.json').then((users: Object[]) => { const realUsers = plainToInstance(User, users); // now each user in realUsers is an instance of User class }); ``` Now you can use `users[0].getName()` and `users[0].isAdult()` methods. ## Installation[⬆](#table-of-contents) ### Node.js[⬆](#table-of-contents) 1. Install module: `npm install class-transformer --save` 2. `reflect-metadata` shim is required, install it too: `npm install reflect-metadata --save` and make sure to import it in a global place, like app.ts: ```typescript import 'reflect-metadata'; ``` 3. ES6 features are used, if you are using old version of node.js you may need to install es6-shim: `npm install es6-shim --save` and import it in a global place like app.ts: ```typescript import 'es6-shim'; ``` ### Browser[⬆](#table-of-contents) 1. Install module: `npm install class-transformer --save` 2. `reflect-metadata` shim is required, install it too: `npm install reflect-metadata --save` add ` ``` If you are using angular 2 you should already have this shim installed. 3. If you are using system.js you may want to add this into `map` and `package` config: ```json { "map": { "class-transformer": "node_modules/class-transformer" }, "packages": { "class-transformer": { "main": "index.js", "defaultExtension": "js" } } } ``` ## Methods[⬆](#table-of-contents) ### plainToInstance[⬆](#table-of-contents) This method transforms a plain javascript object to instance of specific class. ```typescript import { plainToInstance } from 'class-transformer'; let users = plainToInstance(User, userJson); // to convert user plain object a single user. also supports arrays ``` ### plainToClassFromExist[⬆](#table-of-contents) This method transforms a plain object into an instance using an already filled Object which is an instance of the target class. ```typescript const defaultUser = new User(); defaultUser.role = 'user'; let mixedUser = plainToClassFromExist(defaultUser, user); // mixed user should have the value role = user when no value is set otherwise. ``` ### instanceToPlain[⬆](#table-of-contents) This method transforms your class object back to plain javascript object, that can be `JSON.stringify` later. ```typescript import { instanceToPlain } from 'class-transformer'; let photo = instanceToPlain(photo); ``` ### instanceToInstance[⬆](#table-of-contents) This method transforms your class object into a new instance of the class object. This may be treated as deep clone of your objects. ```typescript import { instanceToInstance } from 'class-transformer'; let photo = instanceToInstance(photo); ``` You can also use an `ignoreDecorators` option in transformation options to ignore all decorators your classes are using. ### serialize[⬆](#table-of-contents) You can serialize your model right to json using `serialize` method: ```typescript import { serialize } from 'class-transformer'; let photo = serialize(photo); ``` `serialize` works with both arrays and non-arrays. ### deserialize and deserializeArray[⬆](#table-of-contents) You can deserialize your model from json using the `deserialize` method: ```typescript import { deserialize } from 'class-transformer'; let photo = deserialize(Photo, photo); ``` To make deserialization work with arrays, use the `deserializeArray` method: ```typescript import { deserializeArray } from 'class-transformer'; let photos = deserializeArray(Photo, photos); ``` ## Enforcing type-safe instance[⬆](#table-of-contents) The default behaviour of the `plainToInstance` method is to set _all_ properties from the plain object, even those which are not specified in the class. ```typescript import { plainToInstance } from 'class-transformer'; class User { id: number; firstName: string; lastName: string; } const fromPlainUser = { unkownProp: 'hello there', firstName: 'Umed', lastName: 'Khudoiberdiev', }; console.log(plainToInstance(User, fromPlainUser)); // User { // unkownProp: 'hello there', // firstName: 'Umed', // lastName: 'Khudoiberdiev', // } ``` If this behaviour does not suit your needs, you can use the `excludeExtraneousValues` option in the `plainToInstance` method while _exposing all your class properties_ as a requirement. ```typescript import { Expose, plainToInstance } from 'class-transformer'; class User { @Expose() id: number; @Expose() firstName: string; @Expose() lastName: string; } const fromPlainUser = { unkownProp: 'hello there', firstName: 'Umed', lastName: 'Khudoiberdiev', }; console.log(plainToInstance(User, fromPlainUser, { excludeExtraneousValues: true })); // User { // id: undefined, // firstName: 'Umed', // lastName: 'Khudoiberdiev' // } ``` ## Working with nested objects[⬆](#table-of-contents) When you are trying to transform objects that have nested objects, it's required to known what type of object you are trying to transform. Since Typescript does not have good reflection abilities yet, we should implicitly specify what type of object each property contain. This is done using `@Type` decorator. Lets say we have an album with photos. And we are trying to convert album plain object to class object: ```typescript import { Type, plainToInstance } from 'class-transformer'; export class Album { id: number; name: string; @Type(() => Photo) photos: Photo[]; } export class Photo { id: number; filename: string; } let album = plainToInstance(Album, albumJson); // now album is Album object with Photo objects inside ``` ### Providing more than one type option[⬆](#table-of-contents) In case the nested object can be of different types, you can provide an additional options object, that specifies a discriminator. The discriminator option must define a `property` that holds the subtype name for the object and the possible `subTypes` that the nested object can converted to. A sub type has a `value`, that holds the constructor of the Type and the `name`, that can match with the `property` of the discriminator. Lets say we have an album that has a top photo. But this photo can be of certain different types. And we are trying to convert album plain object to class object. The plain object input has to define the additional property `__type`. This property is removed during transformation by default: **JSON input**: ```json { "id": 1, "name": "foo", "topPhoto": { "id": 9, "filename": "cool_wale.jpg", "depth": 1245, "__type": "underwater" } } ``` ```typescript import { Type, plainToInstance } from 'class-transformer'; export abstract class Photo { id: number; filename: string; } export class Landscape extends Photo { panorama: boolean; } export class Portrait extends Photo { person: Person; } export class UnderWater extends Photo { depth: number; } export class Album { id: number; name: string; @Type(() => Photo, { discriminator: { property: '__type', subTypes: [ { value: Landscape, name: 'landscape' }, { value: Portrait, name: 'portrait' }, { value: UnderWater, name: 'underwater' }, ], }, }) topPhoto: Landscape | Portrait | UnderWater; } let album = plainToInstance(Album, albumJson); // now album is Album object with a UnderWater object without `__type` property. ``` Hint: The same applies for arrays with different sub types. Moreover you can specify `keepDiscriminatorProperty: true` in the options to keep the discriminator property also inside your resulting class. ## Exposing getters and method return values[⬆](#table-of-contents) You can expose what your getter or method return by setting an `@Expose()` decorator to those getters or methods: ```typescript import { Expose } from 'class-transformer'; export class User { id: number; firstName: string; lastName: string; password: string; @Expose() get name() { return this.firstName + ' ' + this.lastName; } @Expose() getFullName() { return this.firstName + ' ' + this.lastName; } } ``` ## Exposing properties with different names[⬆](#table-of-contents) If you want to expose some of the properties with a different name, you can do that by specifying a `name` option to `@Expose` decorator: ```typescript import { Expose } from 'class-transformer'; export class User { @Expose({ name: 'uid' }) id: number; firstName: string; lastName: string; @Expose({ name: 'secretKey' }) password: string; @Expose({ name: 'fullName' }) getFullName() { return this.firstName + ' ' + this.lastName; } } ``` ## Skipping specific properties[⬆](#table-of-contents) Sometimes you want to skip some properties during transformation. This can be done using `@Exclude` decorator: ```typescript import { Exclude } from 'class-transformer'; export class User { id: number; email: string; @Exclude() password: string; } ``` Now when you transform a User, the `password` property will be skipped and not be included in the transformed result. ## Skipping depend of operation[⬆](#table-of-contents) You can control on what operation you will exclude a property. Use `toClassOnly` or `toPlainOnly` options: ```typescript import { Exclude } from 'class-transformer'; export class User { id: number; email: string; @Exclude({ toPlainOnly: true }) password: string; } ``` Now `password` property will be excluded only during `instanceToPlain` operation. Vice versa, use the `toClassOnly` option. ## Skipping all properties of the class[⬆](#table-of-contents) You can skip all properties of the class, and expose only those are needed explicitly: ```typescript import { Exclude, Expose } from 'class-transformer'; @Exclude() export class User { @Expose() id: number; @Expose() email: string; password: string; } ``` Now `id` and `email` will be exposed, and password will be excluded during transformation. Alternatively, you can set exclusion strategy during transformation: ```typescript import { instanceToPlain } from 'class-transformer'; let photo = instanceToPlain(photo, { strategy: 'excludeAll' }); ``` In this case you don't need to `@Exclude()` a whole class. ## Skipping private properties, or some prefixed properties[⬆](#table-of-contents) If you name your private properties with a prefix, lets say with `_`, then you can exclude such properties from transformation too: ```typescript import { instanceToPlain } from 'class-transformer'; let photo = instanceToPlain(photo, { excludePrefixes: ['_'] }); ``` This will skip all properties that start with `_` prefix. You can pass any number of prefixes and all properties that begin with these prefixes will be ignored. For example: ```typescript import { Expose, instanceToPlain } from 'class-transformer'; export class User { id: number; private _firstName: string; private _lastName: string; _password: string; setName(firstName: string, lastName: string) { this._firstName = firstName; this._lastName = lastName; } @Expose() get name() { return this._firstName + ' ' + this._lastName; } } const user = new User(); user.id = 1; user.setName('Johny', 'Cage'); user._password = '123'; const plainUser = instanceToPlain(user, { excludePrefixes: ['_'] }); // here plainUser will be equal to // { id: 1, name: "Johny Cage" } ``` ## Using groups to control excluded properties[⬆](#table-of-contents) You can use groups to control what data will be exposed and what will not be: ```typescript import { Exclude, Expose, instanceToPlain } from 'class-transformer'; export class User { id: number; name: string; @Expose({ groups: ['user', 'admin'] }) // this means that this data will be exposed only to users and admins email: string; @Expose({ groups: ['user'] }) // this means that this data will be exposed only to users password: string; } let user1 = instanceToPlain(user, { groups: ['user'] }); // will contain id, name, email and password let user2 = instanceToPlain(user, { groups: ['admin'] }); // will contain id, name and email ``` ## Using versioning to control exposed and excluded properties[⬆](#table-of-contents) If you are building an API that has different versions, class-transformer has extremely useful tools for that. You can control which properties of your model should be exposed or excluded in what version. Example: ```typescript import { Exclude, Expose, instanceToPlain } from 'class-transformer'; export class User { id: number; name: string; @Expose({ since: 0.7, until: 1 }) // this means that this property will be exposed for version starting from 0.7 until 1 email: string; @Expose({ since: 2.1 }) // this means that this property will be exposed for version starting from 2.1 password: string; } let user1 = instanceToPlain(user, { version: 0.5 }); // will contain id and name let user2 = instanceToPlain(user, { version: 0.7 }); // will contain id, name and email let user3 = instanceToPlain(user, { version: 1 }); // will contain id and name let user4 = instanceToPlain(user, { version: 2 }); // will contain id and name let user5 = instanceToPlain(user, { version: 2.1 }); // will contain id, name and password ``` ## Сonverting date strings into Date objects[⬆](#table-of-contents) Sometimes you have a Date in your plain javascript object received in a string format. And you want to create a real javascript Date object from it. You can do it simply by passing a Date object to the `@Type` decorator: ```typescript import { Type } from 'class-transformer'; export class User { id: number; email: string; password: string; @Type(() => Date) registrationDate: Date; } ``` Same technique can be used with `Number`, `String`, `Boolean` primitive types when you want to convert your values into these types. ## Working with arrays[⬆](#table-of-contents) When you are using arrays you must provide a type of the object that array contains. This type, you specify in a `@Type()` decorator: ```typescript import { Type } from 'class-transformer'; export class Photo { id: number; name: string; @Type(() => Album) albums: Album[]; } ``` You can also use custom array types: ```typescript import { Type } from 'class-transformer'; export class AlbumCollection extends Array { // custom array functions ... } export class Photo { id: number; name: string; @Type(() => Album) albums: AlbumCollection; } ``` Library will handle proper transformation automatically. ES6 collections `Set` and `Map` also require the `@Type` decorator: ```typescript export class Skill { name: string; } export class Weapon { name: string; range: number; } export class Player { name: string; @Type(() => Skill) skills: Set; @Type(() => Weapon) weapons: Map; } ``` ## Additional data transformation[⬆](#table-of-contents) ### Basic usage[⬆](#table-of-contents) You can perform additional data transformation using `@Transform` decorator. For example, you want to make your `Date` object to be a `moment` object when you are transforming object from plain to class: ```typescript import { Transform } from 'class-transformer'; import * as moment from 'moment'; import { Moment } from 'moment'; export class Photo { id: number; @Type(() => Date) @Transform(({ value }) => moment(value), { toClassOnly: true }) date: Moment; } ``` Now when you call `plainToInstance` and send a plain representation of the Photo object, it will convert a date value in your photo object to moment date. `@Transform` decorator also supports groups and versioning. ### Advanced usage[⬆](#table-of-contents) The `@Transform` decorator is given more arguments to let you configure how you want the transformation to be done. ```ts @Transform(({ value, key, obj, type }) => value) ``` | Argument | Description | | --------- | ------------------------------------------------------- | | `value` | The property value before the transformation. | | `key` | The name of the transformed property. | | `obj` | The transformation source object. | | `type` | The transformation type. | | `options` | The options object passed to the transformation method. | ## Other decorators[⬆](#table-of-contents) | Signature | Example | Description | | ------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------- | | `@TransformClassToPlain` | `@TransformClassToPlain({ groups: ["user"] })` | Transform the method return with instanceToPlain and expose the properties on the class. | | `@TransformClassToClass` | `@TransformClassToClass({ groups: ["user"] })` | Transform the method return with instanceToInstance and expose the properties on the class. | | `@TransformPlainToClass` | `@TransformPlainToClass(User, { groups: ["user"] })` | Transform the method return with plainToInstance and expose the properties on the class. | The above decorators accept one optional argument: ClassTransformOptions - The transform options like groups, version, name An example: ```typescript @Exclude() class User { id: number; @Expose() firstName: string; @Expose() lastName: string; @Expose({ groups: ['user.email'] }) email: string; password: string; } class UserController { @TransformClassToPlain({ groups: ['user.email'] }) getUser() { const user = new User(); user.firstName = 'Snir'; user.lastName = 'Segal'; user.password = 'imnosuperman'; return user; } } const controller = new UserController(); const user = controller.getUser(); ``` the `user` variable will contain only firstName,lastName, email properties because they are the exposed variables. email property is also exposed because we metioned the group "user.email". ## Working with generics[⬆](#table-of-contents) Generics are not supported because TypeScript does not have good reflection abilities yet. Once TypeScript team provide us better runtime type reflection tools, generics will be implemented. There are some tweaks however you can use, that maybe can solve your problem. [Checkout this example.](https://github.com/pleerock/class-transformer/tree/master/sample/sample4-generics) ## Implicit type conversion[⬆](#table-of-contents) > **NOTE** If you use class-validator together with class-transformer you propably DON'T want to enable this function. Enables automatic conversion between built-in types based on type information provided by Typescript. Disabled by default. ```ts import { IsString } from 'class-validator'; class MyPayload { @IsString() prop: string; } const result1 = plainToInstance(MyPayload, { prop: 1234 }, { enableImplicitConversion: true }); const result2 = plainToInstance(MyPayload, { prop: 1234 }, { enableImplicitConversion: false }); /** * result1 will be `{ prop: "1234" }` - notice how the prop value has been converted to string. * result2 will be `{ prop: 1234 }` - default behaviour */ ``` ## How does it handle circular references?[⬆](#table-of-contents) Circular references are ignored. For example, if you are transforming class `User` that contains property `photos` with type of `Photo`, and `Photo` contains link `user` to its parent `User`, then `user` will be ignored during transformation. Circular references are not ignored only during `instanceToInstance` operation. ## Example with Angular2[⬆](#table-of-contents) Lets say you want to download users and want them automatically to be mapped to the instances of `User` class. ```typescript import { plainToInstance } from 'class-transformer'; this.http .get('users.json') .map(res => res.json()) .map(res => plainToInstance(User, res as Object[])) .subscribe(users => { // now "users" is type of User[] and each user has getName() and isAdult() methods available console.log(users); }); ``` You can also inject a class `ClassTransformer` as a service in `providers`, and use its methods. Example how to use with angular 2 in [plunker](http://plnkr.co/edit/Mja1ZYAjVySWASMHVB9R). Source code is [here](https://github.com/pleerock/class-transformer-demo). ## Samples[⬆](#table-of-contents) Take a look on samples in [./sample](https://github.com/pleerock/class-transformer/tree/master/sample) for more examples of usages. ## Release notes[⬆](#table-of-contents) See information about breaking changes and release notes [here](https://github.com/typestack/class-transformer/blob/master/CHANGELOG.md). ================================================ FILE: codecov.yml ================================================ coverage: range: 70..100 round: down precision: 2 status: project: default: threshold: 0% paths: - src/**/*.ts comment: off ignore: - testing/**/*.ts - src/**/*.interface.ts ================================================ FILE: docs/SUMMARY.md ================================================ # Table of contents - [Getting Started](pages/01-getting-started.md) - [Basic usage](pages/02-basis-usage.md) ================================================ FILE: docs/pages/01-getting-started.md ================================================ # Getting Started The `class-transformer` package is a zero-dependency utility library helping you to quickly transform class instances to plain objects and vice-versa. It works well with the [`class-validator`][class-validator] library. The main features include: - conditionally transforming object properties - excluding specific properties from the transformed object - exposing properties under a different name on the transformed object - supports both NodeJS and browsers - fully three-shakable - zero external dependencies ## Installation To start using class-transformer install the required packages via NPM: ```bash npm install class-transformer reflect-metadata ``` Import the `reflect-metadata` package at the **first line** of your application: ```ts import 'reflect-metadata'; // Your other imports and initialization code // comes here after you imported the reflect-metadata package! ``` As the last step, you need to enable emitting decorator metadata in your Typescript config. Add these two lines to your `tsconfig.json` file under the `compilerOptions` key: ```json "emitDecoratorMetadata": true, "experimentalDecorators": true, ``` Now you are ready to use class-transformer with Typescript! ## Basic Usage The most basic usage is to transform a class to a plain object: ```ts import { Expose, Exclude, classToInstance } from 'class-transformer'; class User { /** * When transformed to plain the `_id` property will be remapped to `id` * in the plain object. */ @Expose({ name: 'id' }) private _id: string; /** * Expose the `name` property as it is in the plain object. */ @Expose() public name: string; /** * Exclude the `passwordHash` so it won't be included in the plain object. */ @Exclude() public passwordHash: string; } const user = getUserMagically(); // contains: User { _id: '42', name: 'John Snow', passwordHash: '2f55ce082...' } const plain = classToInstance(user); // contains { id: '42', name: 'John Snow' } ``` [class-validator]: https://github.com/typestack/class-validator/ ================================================ FILE: docs/pages/02-basic-usage.md ================================================ # Basic usage There are two main exported functions what can be used for transformations: - `plainToInstance` - transforms a plain object to an instance of the specified class constructor - `instanceToPlain` - transforms a _known_ class instance to a plain object Both function transforms the source object to the target via applying the metadata registered by the decorators on the class definition. The four main decorators are: - `@Expose` specifies how expose the given property on the plain object - `@Exclude` marks the property as skipped, so it won't show up in the transformation - `@Transform` allows specifying a custom transformation on the property via a custom handler - `@Type` decorator explicitly sets the type of the property, during the transformation `class-transformer` will attempt to create an instance of the specified type You must always decorate all your properties with an `@Expose` or `@Exclude` decorator. > **NOTE:** It's important to remember `class-transformer` will call the target type with am empty constructor, so if > you are using a type what requires special setup, you need to use a `@Transform` decorator and create the instance yourself. ================================================ FILE: jest.config.js ================================================ module.exports = { preset: 'ts-jest', testEnvironment: 'node', collectCoverageFrom: ['src/**/*.ts', '!src/**/index.ts', '!src/**/*.interface.ts'], globals: { 'ts-jest': { tsconfig: 'tsconfig.spec.json', }, }, }; ================================================ FILE: package.json ================================================ { "name": "class-transformer", "version": "0.5.1", "description": "Proper decorator-based transformation / serialization / deserialization of plain javascript objects to class constructors", "author": "TypeStack contributors", "license": "MIT", "readmeFilename": "README.md", "sideEffects": false, "main": "./cjs/index.js", "module": "./esm5/index.js", "es2015": "./esm2015/index.js", "typings": "./types/index.d.ts", "repository": { "type": "git", "url": "https://github.com/typestack/class-transformer.git" }, "tags": [ "serialization", "deserialization", "serializer", "typescript", "object-to-class", "typescript-serializer" ], "scripts": { "build": "npm run build:cjs", "build:clean": "rimraf build", "build:es2015": "tsc --project tsconfig.prod.esm2015.json", "build:esm5": "tsc --project tsconfig.prod.esm5.json", "build:cjs": "tsc --project tsconfig.prod.cjs.json", "build:umd": "rollup --config rollup.config.js", "build:types": "tsc --project tsconfig.prod.types.json", "prettier:fix": "prettier --write \"**/*.{ts,md}\"", "prettier:check": "prettier --check \"**/*.{ts,md}\"", "lint:fix": "eslint --max-warnings 0 --fix --ext .ts src/", "lint:check": "eslint --max-warnings 0 --ext .ts src/", "test": "jest --coverage --verbose", "test:watch": "jest --watch", "test:ci": "jest --runInBand --no-cache --coverage --verbose" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.md": [ "npm run prettier:fix" ], "*.ts": [ "npm run prettier:fix" ] }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.5", "@rollup/plugin-node-resolve": "^15.2.2", "@types/jest": "^27.5.0", "@types/node": "^20.8.5", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "eslint": "^7.32.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-jest": "^26.8.7", "husky": "^4.3.8", "jest": "^26.6.3", "lint-staged": "^14.0.1", "prettier": "^2.8.8", "reflect-metadata": "0.1.13", "rimraf": "5.0.5", "rollup": "^2.79.1", "rollup-plugin-terser": "^7.0.2", "ts-jest": "^26.5.6", "ts-node": "^10.9.1", "typescript": "^4.9.5" } } ================================================ FILE: rollup.config.js ================================================ import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import { terser } from 'rollup-plugin-terser'; export default { input: 'build/esm5/index.js', output: [ { name: 'ClassTransformer', format: 'umd', file: 'build/bundles/class-transformer.umd.js', sourcemap: true, }, { name: 'ClassTransformer', format: 'umd', file: 'build/bundles/class-transformer.umd.min.js', sourcemap: true, plugins: [terser()], }, ], plugins: [commonjs(), nodeResolve()], }; ================================================ FILE: sample/sample1-simple-usage/Album.ts ================================================ import { Type, Exclude } from '../../src/decorators'; import { Photo } from './Photo'; export class Album { id: string; @Exclude() name: string; @Type(() => Photo) photos: Photo[]; } ================================================ FILE: sample/sample1-simple-usage/Photo.ts ================================================ import { Type } from '../../src/decorators'; import { Album } from './Album'; import { User } from './User'; export class Photo { id: string; filename: string; description: string; tags: string[]; @Type(() => User) author: User; @Type(() => Album) albums: Album[]; get name() { return this.id + '_' + this.filename; } getAlbums() { console.log('this is not serialized/deserialized'); return this.albums; } } ================================================ FILE: sample/sample1-simple-usage/User.ts ================================================ import { Type } from '../../src/decorators'; export class User { @Type(() => Number) id: number; firstName: string; lastName: string; @Type(() => Date) registrationDate: Date; } ================================================ FILE: sample/sample1-simple-usage/app.ts ================================================ import 'es6-shim'; import 'reflect-metadata'; import { plainToClass, classToPlain } from '../../src/index'; import { Photo } from './Photo'; // check deserialization let photoJson = { id: '1', filename: 'myphoto.jpg', description: 'about my photo', tags: ['me', 'iam'], author: { id: '2', firstName: 'Johny', lastName: 'Cage', }, albums: [ { id: '1', name: 'My life', }, { id: '2', name: 'My young years', }, ], }; let photo = plainToClass(Photo, photoJson); console.log('deserialized object: ', photo); // now check serialization let newPhotoJson = classToPlain(photo); console.log('serialized object: ', newPhotoJson); // try to deserialize an array console.log('-------------------------------'); let photosJson = [ { id: '1', filename: 'myphoto.jpg', description: 'about my photo', author: { id: '2', firstName: 'Johny', lastName: 'Cage', registrationDate: '1995-12-17T03:24:00', }, albums: [ { id: '1', name: 'My life', }, { id: '2', name: 'My young years', }, ], }, { id: '2', filename: 'hisphoto.jpg', description: 'about his photo', author: { id: '2', firstName: 'Johny', lastName: 'Cage', }, albums: [ { id: '1', name: 'My life', }, { id: '2', name: 'My young years', }, ], }, ]; let photos = plainToClass(Photo, photosJson); console.log('deserialized array: ', photos); // now check array serialization let newPhotosJson = classToPlain(photos); console.log('serialized array: ', newPhotosJson); ================================================ FILE: sample/sample2-iheritance/Album.ts ================================================ import { Type, Exclude } from '../../src/decorators'; import { Photo } from './Photo'; import { Authorable } from './Authorable'; export class Album extends Authorable { id: string; @Exclude() name: string; @Type(() => Photo) photos: Photo[]; } ================================================ FILE: sample/sample2-iheritance/Authorable.ts ================================================ import { Type, Exclude } from '../../src/decorators'; import { User } from './User'; export class Authorable { authorName: string; @Exclude() authorEmail: string; @Type(() => User) author: User; } ================================================ FILE: sample/sample2-iheritance/Photo.ts ================================================ import { Type, Exclude } from '../../src/decorators'; import { Album } from './Album'; import { Authorable } from './Authorable'; export class Photo extends Authorable { id: string; filename: string; description: string; @Exclude() // this will ignore skipping inherited from Authorable class authorEmail: string; @Type(() => Album) albums: Album[]; } ================================================ FILE: sample/sample2-iheritance/User.ts ================================================ import { Type } from '../../src/decorators'; export class User { @Type(() => Number) id: number; firstName: string; lastName: string; @Type(() => Date) registrationDate: Date; } ================================================ FILE: sample/sample2-iheritance/app.ts ================================================ import 'es6-shim'; import 'reflect-metadata'; import { classToPlain, plainToClass } from '../../src/index'; import { Photo } from './Photo'; let photoJson = { id: '1', filename: 'myphoto.jpg', description: 'about my photo', authorName: 'Johny.Cage', authorEmail: 'johny@cage.com', author: { id: '2', firstName: 'Johny', lastName: 'Cage', }, albums: [ { id: '1', authorName: 'Johny.Cage', authorEmail: 'johny@cage.com', name: 'My life', }, { id: '2', authorName: 'Johny.Cage', authorEmail: 'johny@cage.com', name: 'My young years', }, ], }; let photo = plainToClass(Photo, photoJson); console.log('deserialized object: ', photo); // now check serialization let newPhotoJson = classToPlain(photo); console.log('serialized object: ', newPhotoJson); // try to deserialize an array console.log('-------------------------------'); let photosJson = [ { id: '1', filename: 'myphoto.jpg', description: 'about my photo', author: { id: '2', firstName: 'Johny', lastName: 'Cage', registrationDate: '1995-12-17T03:24:00', }, albums: [ { id: '1', name: 'My life', }, { id: '2', name: 'My young years', }, ], }, { id: '2', filename: 'hisphoto.jpg', description: 'about his photo', author: { id: '2', firstName: 'Johny', lastName: 'Cage', }, albums: [ { id: '1', name: 'My life', }, { id: '2', name: 'My young years', }, ], }, ]; let photos = plainToClass(Photo, photosJson); console.log('deserialized array: ', photos); // now check array serialization let newPhotosJson = classToPlain(photos); console.log('serialized array: ', newPhotosJson); ================================================ FILE: sample/sample3-custom-arrays/Album.ts ================================================ export class Album { id: string; name: string; } ================================================ FILE: sample/sample3-custom-arrays/AlbumArray.ts ================================================ import { Album } from './Album'; export class AlbumArray extends Array { findByName(name: string) { return this.find(album => album.name === name); } } ================================================ FILE: sample/sample3-custom-arrays/Photo.ts ================================================ import { Album } from './Album'; import { AlbumArray } from './AlbumArray'; import { Type } from '../../src/decorators'; export class Photo { id: string; filename: string; description: string; tags: string[]; @Type(() => Album) albums: AlbumArray; } ================================================ FILE: sample/sample3-custom-arrays/app.ts ================================================ import 'es6-shim'; import 'reflect-metadata'; import { classToPlain, plainToClass } from '../../src/index'; import { Photo } from './Photo'; // check deserialization let photoJson = { id: '1', filename: 'myphoto.jpg', description: 'about my photo', tags: ['me', 'iam'], albums: [ { id: '1', name: 'My life', }, { id: '2', name: 'My young years', }, ], }; let photo = plainToClass(Photo, photoJson); console.log('deserialized object: ', photo); console.log('-----------------------------'); console.log('Trying to find album: ', photo.albums.findByName('My life')); console.log('-----------------------------'); // now check serialization let newPhotoJson = classToPlain(photo); console.log('serialized object: ', newPhotoJson); console.log('-----------------------------'); ================================================ FILE: sample/sample4-generics/SimpleCollection.ts ================================================ export class SimpleCollection { items: T[]; count: number; } ================================================ FILE: sample/sample4-generics/SuperCollection.ts ================================================ import { Type, Exclude } from '../../src/decorators'; export class SuperCollection { @Exclude() private type: Function; @Type(options => { return (options.newObject as SuperCollection).type; }) items: T[]; count: number; constructor(type: Function) { this.type = type; } } ================================================ FILE: sample/sample4-generics/User.ts ================================================ import { Exclude } from '../../src/decorators'; export class User { id: number; firstName: string; lastName: string; @Exclude() password: string; constructor(id: number, firstName: string, lastName: string, password: string) { this.id = id; this.firstName = firstName; this.lastName = lastName; this.password = password; } get name() { return this.firstName + ' ' + this.lastName; } } ================================================ FILE: sample/sample4-generics/app.ts ================================================ import 'es6-shim'; import 'reflect-metadata'; import { SimpleCollection } from './SimpleCollection'; import { User } from './User'; import { classToPlain, plainToClass, plainToClassFromExist } from '../../src/index'; import { SuperCollection } from './SuperCollection'; let collection = new SimpleCollection(); collection.items = [new User(1, 'Johny', 'Cage', '*******'), new User(2, 'Dima', 'Cage', '*******')]; collection.count = 2; // using generics works only for classToPlain operations, since in runtime we can // "guess" type without type provided only we have a constructor, not plain object. // console.log(classToPlain(collection)); // alternatively you can use factory method let collectionJson = { items: [ { id: 1, firstName: 'Johny', lastName: 'Cage', password: '*******', }, { id: 2, firstName: 'Dima', lastName: 'Cage', password: '*******', }, ], }; console.log(plainToClassFromExist(new SuperCollection(User), collectionJson)); ================================================ FILE: sample/sample5-custom-transformer/User.ts ================================================ import { Type, Transform } from '../../src/decorators'; import * as moment from 'moment'; export class User { id: number; name: string; @Type(() => Date) @Transform(value => value.toString(), { toPlainOnly: true }) @Transform(value => moment(value), { toClassOnly: true }) date: Date; } ================================================ FILE: sample/sample5-custom-transformer/app.ts ================================================ import 'es6-shim'; import 'reflect-metadata'; import { plainToClass, classToPlain } from '../../src/index'; import { User } from './User'; let userJson = { id: 1, name: 'Johny Cage', date: new Date().valueOf(), }; console.log(plainToClass(User, userJson)); const user = new User(); user.id = 1; user.name = 'Johny Cage'; user.date = new Date(); console.log(classToPlain(user)); ================================================ FILE: src/ClassTransformer.ts ================================================ import { ClassTransformOptions } from './interfaces'; import { TransformOperationExecutor } from './TransformOperationExecutor'; import { TransformationType } from './enums'; import { ClassConstructor } from './interfaces'; import { defaultOptions } from './constants/default-options.constant'; export class ClassTransformer { // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Converts class (constructor) object to plain (literal) object. Also works with arrays. */ instanceToPlain>(object: T, options?: ClassTransformOptions): Record; instanceToPlain>(object: T[], options?: ClassTransformOptions): Record[]; instanceToPlain>( object: T | T[], options?: ClassTransformOptions ): Record | Record[] { const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_PLAIN, { ...defaultOptions, ...options, }); return executor.transform(undefined, object, undefined, undefined, undefined, undefined); } /** * Converts class (constructor) object to plain (literal) object. * Uses given plain object as source object (it means fills given plain object with data from class object). * Also works with arrays. */ classToPlainFromExist, P>( object: T, plainObject: P, options?: ClassTransformOptions ): T; classToPlainFromExist, P>( object: T, plainObjects: P[], options?: ClassTransformOptions ): T[]; classToPlainFromExist, P>( object: T, plainObject: P | P[], options?: ClassTransformOptions ): T | T[] { const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_PLAIN, { ...defaultOptions, ...options, }); return executor.transform(plainObject, object, undefined, undefined, undefined, undefined); } /** * Converts plain (literal) object to class (constructor) object. Also works with arrays. */ plainToInstance, V extends Array>( cls: ClassConstructor, plain: V, options?: ClassTransformOptions ): T[]; plainToInstance, V>( cls: ClassConstructor, plain: V, options?: ClassTransformOptions ): T; plainToInstance, V>( cls: ClassConstructor, plain: V | V[], options?: ClassTransformOptions ): T | T[] { const executor = new TransformOperationExecutor(TransformationType.PLAIN_TO_CLASS, { ...defaultOptions, ...options, }); return executor.transform(undefined, plain, cls, undefined, undefined, undefined); } /** * Converts plain (literal) object to class (constructor) object. * Uses given object as source object (it means fills given object with data from plain object). * Also works with arrays. */ plainToClassFromExist, V extends Array>( clsObject: T, plain: V, options?: ClassTransformOptions ): T; plainToClassFromExist, V>(clsObject: T, plain: V, options?: ClassTransformOptions): T[]; plainToClassFromExist, V>( clsObject: T, plain: V | V[], options?: ClassTransformOptions ): T | T[] { const executor = new TransformOperationExecutor(TransformationType.PLAIN_TO_CLASS, { ...defaultOptions, ...options, }); return executor.transform(clsObject, plain, undefined, undefined, undefined, undefined); } /** * Converts class (constructor) object to new class (constructor) object. Also works with arrays. */ instanceToInstance(object: T, options?: ClassTransformOptions): T; instanceToInstance(object: T[], options?: ClassTransformOptions): T[]; instanceToInstance(object: T | T[], options?: ClassTransformOptions): T | T[] { const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_CLASS, { ...defaultOptions, ...options, }); return executor.transform(undefined, object, undefined, undefined, undefined, undefined); } /** * Converts class (constructor) object to plain (literal) object. * Uses given plain object as source object (it means fills given plain object with data from class object). * Also works with arrays. */ classToClassFromExist(object: T, fromObject: T, options?: ClassTransformOptions): T; classToClassFromExist(object: T, fromObjects: T[], options?: ClassTransformOptions): T[]; classToClassFromExist(object: T, fromObject: T | T[], options?: ClassTransformOptions): T | T[] { const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_CLASS, { ...defaultOptions, ...options, }); return executor.transform(fromObject, object, undefined, undefined, undefined, undefined); } /** * Serializes given object to a JSON string. */ serialize(object: T, options?: ClassTransformOptions): string; serialize(object: T[], options?: ClassTransformOptions): string; serialize(object: T | T[], options?: ClassTransformOptions): string { return JSON.stringify(this.instanceToPlain(object, options)); } /** * Deserializes given JSON string to a object of the given class. */ deserialize(cls: ClassConstructor, json: string, options?: ClassTransformOptions): T { const jsonObject: T = JSON.parse(json); return this.plainToInstance(cls, jsonObject, options); } /** * Deserializes given JSON string to an array of objects of the given class. */ deserializeArray(cls: ClassConstructor, json: string, options?: ClassTransformOptions): T[] { const jsonObject: any[] = JSON.parse(json); return this.plainToInstance(cls, jsonObject, options); } } ================================================ FILE: src/MetadataStorage.ts ================================================ import { TypeMetadata, ExposeMetadata, ExcludeMetadata, TransformMetadata } from './interfaces'; import { TransformationType } from './enums'; /** * Storage all library metadata. */ export class MetadataStorage { // ------------------------------------------------------------------------- // Properties // ------------------------------------------------------------------------- private _typeMetadatas = new Map>(); private _transformMetadatas = new Map>(); private _exposeMetadatas = new Map>(); private _excludeMetadatas = new Map>(); private _ancestorsMap = new Map(); // ------------------------------------------------------------------------- // Adder Methods // ------------------------------------------------------------------------- addTypeMetadata(metadata: TypeMetadata): void { if (!this._typeMetadatas.has(metadata.target)) { this._typeMetadatas.set(metadata.target, new Map()); } this._typeMetadatas.get(metadata.target).set(metadata.propertyName, metadata); } addTransformMetadata(metadata: TransformMetadata): void { if (!this._transformMetadatas.has(metadata.target)) { this._transformMetadatas.set(metadata.target, new Map()); } if (!this._transformMetadatas.get(metadata.target).has(metadata.propertyName)) { this._transformMetadatas.get(metadata.target).set(metadata.propertyName, []); } this._transformMetadatas.get(metadata.target).get(metadata.propertyName).push(metadata); } addExposeMetadata(metadata: ExposeMetadata): void { if (!this._exposeMetadatas.has(metadata.target)) { this._exposeMetadatas.set(metadata.target, new Map()); } this._exposeMetadatas.get(metadata.target).set(metadata.propertyName, metadata); } addExcludeMetadata(metadata: ExcludeMetadata): void { if (!this._excludeMetadatas.has(metadata.target)) { this._excludeMetadatas.set(metadata.target, new Map()); } this._excludeMetadatas.get(metadata.target).set(metadata.propertyName, metadata); } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- findTransformMetadatas( target: Function, propertyName: string, transformationType: TransformationType ): TransformMetadata[] { return this.findMetadatas(this._transformMetadatas, target, propertyName).filter(metadata => { if (!metadata.options) return true; if (metadata.options.toClassOnly === true && metadata.options.toPlainOnly === true) return true; if (metadata.options.toClassOnly === true) { return ( transformationType === TransformationType.CLASS_TO_CLASS || transformationType === TransformationType.PLAIN_TO_CLASS ); } if (metadata.options.toPlainOnly === true) { return transformationType === TransformationType.CLASS_TO_PLAIN; } return true; }); } findExcludeMetadata(target: Function, propertyName: string): ExcludeMetadata { return this.findMetadata(this._excludeMetadatas, target, propertyName); } findExposeMetadata(target: Function, propertyName: string): ExposeMetadata { return this.findMetadata(this._exposeMetadatas, target, propertyName); } findExposeMetadataByCustomName(target: Function, name: string): ExposeMetadata { return this.getExposedMetadatas(target).find(metadata => { return metadata.options && metadata.options.name === name; }); } findTypeMetadata(target: Function, propertyName: string): TypeMetadata { return this.findMetadata(this._typeMetadatas, target, propertyName); } getStrategy(target: Function): 'excludeAll' | 'exposeAll' | 'none' { const excludeMap = this._excludeMetadatas.get(target); const exclude = excludeMap && excludeMap.get(undefined); const exposeMap = this._exposeMetadatas.get(target); const expose = exposeMap && exposeMap.get(undefined); if ((exclude && expose) || (!exclude && !expose)) return 'none'; return exclude ? 'excludeAll' : 'exposeAll'; } getExposedMetadatas(target: Function): ExposeMetadata[] { return this.getMetadata(this._exposeMetadatas, target); } getExcludedMetadatas(target: Function): ExcludeMetadata[] { return this.getMetadata(this._excludeMetadatas, target); } getExposedProperties(target: Function, transformationType: TransformationType): string[] { return this.getExposedMetadatas(target) .filter(metadata => { if (!metadata.options) return true; if (metadata.options.toClassOnly === true && metadata.options.toPlainOnly === true) return true; if (metadata.options.toClassOnly === true) { return ( transformationType === TransformationType.CLASS_TO_CLASS || transformationType === TransformationType.PLAIN_TO_CLASS ); } if (metadata.options.toPlainOnly === true) { return transformationType === TransformationType.CLASS_TO_PLAIN; } return true; }) .map(metadata => metadata.propertyName); } getExcludedProperties(target: Function, transformationType: TransformationType): string[] { return this.getExcludedMetadatas(target) .filter(metadata => { if (!metadata.options) return true; if (metadata.options.toClassOnly === true && metadata.options.toPlainOnly === true) return true; if (metadata.options.toClassOnly === true) { return ( transformationType === TransformationType.CLASS_TO_CLASS || transformationType === TransformationType.PLAIN_TO_CLASS ); } if (metadata.options.toPlainOnly === true) { return transformationType === TransformationType.CLASS_TO_PLAIN; } return true; }) .map(metadata => metadata.propertyName); } clear(): void { this._typeMetadatas.clear(); this._exposeMetadatas.clear(); this._excludeMetadatas.clear(); this._ancestorsMap.clear(); } // ------------------------------------------------------------------------- // Private Methods // ------------------------------------------------------------------------- private getMetadata( metadatas: Map>, target: Function ): T[] { const metadataFromTargetMap = metadatas.get(target); let metadataFromTarget: T[]; if (metadataFromTargetMap) { metadataFromTarget = Array.from(metadataFromTargetMap.values()).filter(meta => meta.propertyName !== undefined); } const metadataFromAncestors: T[] = []; for (const ancestor of this.getAncestors(target)) { const ancestorMetadataMap = metadatas.get(ancestor); if (ancestorMetadataMap) { const metadataFromAncestor = Array.from(ancestorMetadataMap.values()).filter( meta => meta.propertyName !== undefined ); metadataFromAncestors.push(...metadataFromAncestor); } } return metadataFromAncestors.concat(metadataFromTarget || []); } private findMetadata( metadatas: Map>, target: Function, propertyName: string ): T { const metadataFromTargetMap = metadatas.get(target); if (metadataFromTargetMap) { const metadataFromTarget = metadataFromTargetMap.get(propertyName); if (metadataFromTarget) { return metadataFromTarget; } } for (const ancestor of this.getAncestors(target)) { const ancestorMetadataMap = metadatas.get(ancestor); if (ancestorMetadataMap) { const ancestorResult = ancestorMetadataMap.get(propertyName); if (ancestorResult) { return ancestorResult; } } } return undefined; } private findMetadatas( metadatas: Map>, target: Function, propertyName: string ): T[] { const metadataFromTargetMap = metadatas.get(target); let metadataFromTarget: T[]; if (metadataFromTargetMap) { metadataFromTarget = metadataFromTargetMap.get(propertyName); } const metadataFromAncestorsTarget: T[] = []; for (const ancestor of this.getAncestors(target)) { const ancestorMetadataMap = metadatas.get(ancestor); if (ancestorMetadataMap) { if (ancestorMetadataMap.has(propertyName)) { metadataFromAncestorsTarget.push(...ancestorMetadataMap.get(propertyName)); } } } return metadataFromAncestorsTarget .slice() .reverse() .concat((metadataFromTarget || []).slice().reverse()); } private getAncestors(target: Function): Function[] { if (!target) return []; if (!this._ancestorsMap.has(target)) { const ancestors: Function[] = []; for ( let baseClass = Object.getPrototypeOf(target.prototype.constructor); typeof baseClass.prototype !== 'undefined'; baseClass = Object.getPrototypeOf(baseClass.prototype.constructor) ) { ancestors.push(baseClass); } this._ancestorsMap.set(target, ancestors); } return this._ancestorsMap.get(target); } } ================================================ FILE: src/TransformOperationExecutor.ts ================================================ import { defaultMetadataStorage } from './storage'; import { ClassTransformOptions, TypeHelpOptions, TypeMetadata, TypeOptions } from './interfaces'; import { TransformationType } from './enums'; import { getGlobal, isPromise } from './utils'; function instantiateArrayType(arrayType: Function): Array | Set { const array = new (arrayType as any)(); if (!(array instanceof Set) && !('push' in array)) { return []; } return array; } export class TransformOperationExecutor { // ------------------------------------------------------------------------- // Private Properties // ------------------------------------------------------------------------- private recursionStack = new Set>(); // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(private transformationType: TransformationType, private options: ClassTransformOptions) {} // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- transform( source: Record | Record[] | any, value: Record | Record[] | any, targetType: Function | TypeMetadata, arrayType: Function, isMap: boolean, level: number = 0 ): any { if (Array.isArray(value) || value instanceof Set) { const newValue = arrayType && this.transformationType === TransformationType.PLAIN_TO_CLASS ? instantiateArrayType(arrayType) : []; (value as any[]).forEach((subValue, index) => { const subSource = source ? source[index] : undefined; if (!this.options.enableCircularCheck || !this.isCircular(subValue)) { let realTargetType; if ( typeof targetType !== 'function' && targetType && targetType.options && targetType.options.discriminator && targetType.options.discriminator.property && targetType.options.discriminator.subTypes ) { if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { realTargetType = targetType.options.discriminator.subTypes.find( subType => subType.name === subValue[(targetType as { options: TypeOptions }).options.discriminator.property] ); const options: TypeHelpOptions = { newObject: newValue, object: subValue, property: undefined }; const newType = targetType.typeFunction(options); realTargetType === undefined ? (realTargetType = newType) : (realTargetType = realTargetType.value); if (!targetType.options.keepDiscriminatorProperty) delete subValue[targetType.options.discriminator.property]; } if (this.transformationType === TransformationType.CLASS_TO_CLASS) { realTargetType = subValue.constructor; } if (this.transformationType === TransformationType.CLASS_TO_PLAIN) { subValue[targetType.options.discriminator.property] = targetType.options.discriminator.subTypes.find( subType => subType.value === subValue.constructor ).name; } } else { realTargetType = targetType; } const value = this.transform( subSource, subValue, realTargetType, undefined, subValue instanceof Map, level + 1 ); if (newValue instanceof Set) { newValue.add(value); } else { newValue.push(value); } } else if (this.transformationType === TransformationType.CLASS_TO_CLASS) { if (newValue instanceof Set) { newValue.add(subValue); } else { newValue.push(subValue); } } }); return newValue; } else if (targetType === String && !isMap) { if (value === null || value === undefined) return value; return String(value); } else if (targetType === Number && !isMap) { if (value === null || value === undefined) return value; return Number(value); } else if (targetType === Boolean && !isMap) { if (value === null || value === undefined) return value; return Boolean(value); } else if ((targetType === Date || value instanceof Date) && !isMap) { if (value instanceof Date) { return new Date(value.valueOf()); } if (value === null || value === undefined) return value; return new Date(value); } else if (!!getGlobal().Buffer && (targetType === Buffer || value instanceof Buffer) && !isMap) { if (value === null || value === undefined) return value; return Buffer.from(value); } else if (isPromise(value) && !isMap) { return new Promise((resolve, reject) => { value.then( (data: any) => resolve(this.transform(undefined, data, targetType, undefined, undefined, level + 1)), reject ); }); } else if (!isMap && value !== null && typeof value === 'object' && typeof value.then === 'function') { // Note: We should not enter this, as promise has been handled above // This option simply returns the Promise preventing a JS error from happening and should be an inaccessible path. return value; // skip promise transformation } else if (typeof value === 'object' && value !== null) { // try to guess the type if (!targetType && value.constructor !== Object /* && TransformationType === TransformationType.CLASS_TO_PLAIN*/) if (!Array.isArray(value) && value.constructor === Array) { // Somebody attempts to convert special Array like object to Array, eg: // const evilObject = { '100000000': '100000000', __proto__: [] }; // This could be used to cause Denial-of-service attack so we don't allow it. // See prevent-array-bomb.spec.ts for more details. } else { // We are good we can use the built-in constructor targetType = value.constructor; } if (!targetType && source) targetType = source.constructor; if (this.options.enableCircularCheck) { // add transformed type to prevent circular references this.recursionStack.add(value); } const keys = this.getKeys(targetType as Function, value, isMap); let newValue: any = source ? source : {}; if ( !source && (this.transformationType === TransformationType.PLAIN_TO_CLASS || this.transformationType === TransformationType.CLASS_TO_CLASS) ) { if (isMap) { newValue = new Map(); } else if (targetType) { newValue = new (targetType as any)(); } else { newValue = {}; } } // traverse over keys for (const key of keys) { if (key === '__proto__' || key === 'constructor') { continue; } const valueKey = key; let newValueKey = key, propertyName = key; if (!this.options.ignoreDecorators && targetType) { if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { const exposeMetadata = defaultMetadataStorage.findExposeMetadataByCustomName(targetType as Function, key); if (exposeMetadata) { propertyName = exposeMetadata.propertyName; newValueKey = exposeMetadata.propertyName; } } else if ( this.transformationType === TransformationType.CLASS_TO_PLAIN || this.transformationType === TransformationType.CLASS_TO_CLASS ) { const exposeMetadata = defaultMetadataStorage.findExposeMetadata(targetType as Function, key); if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name) { newValueKey = exposeMetadata.options.name; } } } // get a subvalue let subValue: any = undefined; if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { /** * This section is added for the following report: * https://github.com/typestack/class-transformer/issues/596 * * We should not call functions or constructors when transforming to class. */ subValue = value[valueKey]; } else { if (value instanceof Map) { subValue = value.get(valueKey); } else if (value[valueKey] instanceof Function) { subValue = value[valueKey](); } else { subValue = value[valueKey]; } } // determine a type let type: any = undefined, isSubValueMap = subValue instanceof Map; if (targetType && isMap) { type = targetType; } else if (targetType) { const metadata = defaultMetadataStorage.findTypeMetadata(targetType as Function, propertyName); if (metadata) { const options: TypeHelpOptions = { newObject: newValue, object: value, property: propertyName }; const newType = metadata.typeFunction ? metadata.typeFunction(options) : metadata.reflectedType; if ( metadata.options && metadata.options.discriminator && metadata.options.discriminator.property && metadata.options.discriminator.subTypes ) { if (!(value[valueKey] instanceof Array)) { if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { type = metadata.options.discriminator.subTypes.find(subType => { if (subValue && subValue instanceof Object && metadata.options.discriminator.property in subValue) { return subType.name === subValue[metadata.options.discriminator.property]; } }); type === undefined ? (type = newType) : (type = type.value); if (!metadata.options.keepDiscriminatorProperty) { if (subValue && subValue instanceof Object && metadata.options.discriminator.property in subValue) { delete subValue[metadata.options.discriminator.property]; } } } if (this.transformationType === TransformationType.CLASS_TO_CLASS) { type = subValue.constructor; } if (this.transformationType === TransformationType.CLASS_TO_PLAIN) { if (subValue) { subValue[metadata.options.discriminator.property] = metadata.options.discriminator.subTypes.find( subType => subType.value === subValue.constructor ).name; } } } else { type = metadata; } } else { type = newType; } isSubValueMap = isSubValueMap || metadata.reflectedType === Map; } else if (this.options.targetMaps) { // try to find a type in target maps this.options.targetMaps .filter(map => map.target === targetType && !!map.properties[propertyName]) .forEach(map => (type = map.properties[propertyName])); } else if ( this.options.enableImplicitConversion && this.transformationType === TransformationType.PLAIN_TO_CLASS ) { // if we have no registererd type via the @Type() decorator then we check if we have any // type declarations in reflect-metadata (type declaration is emited only if some decorator is added to the property.) const reflectedType = (Reflect as any).getMetadata( 'design:type', (targetType as Function).prototype, propertyName ); if (reflectedType) { type = reflectedType; } } } // if value is an array try to get its custom array type const arrayType = Array.isArray(value[valueKey]) ? this.getReflectedType(targetType as Function, propertyName) : undefined; // const subValueKey = TransformationType === TransformationType.PLAIN_TO_CLASS && newKeyName ? newKeyName : key; const subSource = source ? source[valueKey] : undefined; // if its deserialization then type if required // if we uncomment this types like string[] will not work // if (this.transformationType === TransformationType.PLAIN_TO_CLASS && !type && subValue instanceof Object && !(subValue instanceof Date)) // throw new Error(`Cannot determine type for ${(targetType as any).name }.${propertyName}, did you forget to specify a @Type?`); // if newValue is a source object that has method that match newKeyName then skip it if (newValue.constructor.prototype) { const descriptor = this.getPropertyDescriptor(newValue.constructor.prototype, newValueKey); if ( (this.transformationType === TransformationType.PLAIN_TO_CLASS || this.transformationType === TransformationType.CLASS_TO_CLASS) && // eslint-disable-next-line @typescript-eslint/unbound-method ((descriptor && !descriptor.set) || newValue[newValueKey] instanceof Function) ) // || TransformationType === TransformationType.CLASS_TO_CLASS continue; } if (!this.options.enableCircularCheck || !this.isCircular(subValue)) { const transformKey = this.transformationType === TransformationType.PLAIN_TO_CLASS ? newValueKey : key; let finalValue; if (this.transformationType === TransformationType.CLASS_TO_PLAIN) { // Get original value finalValue = value[transformKey]; // Apply custom transformation finalValue = this.applyCustomTransformations( finalValue, targetType as Function, transformKey, value, this.transformationType ); // If nothing change, it means no custom transformation was applied, so use the subValue. finalValue = value[transformKey] === finalValue ? subValue : finalValue; // Apply the default transformation finalValue = this.transform(subSource, finalValue, type, arrayType, isSubValueMap, level + 1); } else { if (subValue === undefined && this.options.exposeDefaultValues) { // Set default value if nothing provided finalValue = newValue[newValueKey]; } else { finalValue = this.transform(subSource, subValue, type, arrayType, isSubValueMap, level + 1); finalValue = this.applyCustomTransformations( finalValue, targetType as Function, transformKey, value, this.transformationType ); } } if (finalValue !== undefined || this.options.exposeUnsetFields) { if (newValue instanceof Map) { newValue.set(newValueKey, finalValue); } else { newValue[newValueKey] = finalValue; } } } else if (this.transformationType === TransformationType.CLASS_TO_CLASS) { let finalValue = subValue; finalValue = this.applyCustomTransformations( finalValue, targetType as Function, key, value, this.transformationType ); if (finalValue !== undefined || this.options.exposeUnsetFields) { if (newValue instanceof Map) { newValue.set(newValueKey, finalValue); } else { newValue[newValueKey] = finalValue; } } } } if (this.options.enableCircularCheck) { this.recursionStack.delete(value); } return newValue; } else { return value; } } private applyCustomTransformations( value: any, target: Function, key: string, obj: any, transformationType: TransformationType ): boolean { let metadatas = defaultMetadataStorage.findTransformMetadatas(target, key, this.transformationType); // apply versioning options if (this.options.version !== undefined) { metadatas = metadatas.filter(metadata => { if (!metadata.options) return true; return this.checkVersion(metadata.options.since, metadata.options.until); }); } // apply grouping options if (this.options.groups && this.options.groups.length) { metadatas = metadatas.filter(metadata => { if (!metadata.options) return true; return this.checkGroups(metadata.options.groups); }); } else { metadatas = metadatas.filter(metadata => { return !metadata.options || !metadata.options.groups || !metadata.options.groups.length; }); } metadatas.forEach(metadata => { value = metadata.transformFn({ value, key, obj, type: transformationType, options: this.options }); }); return value; } // preventing circular references private isCircular(object: Record): boolean { return this.recursionStack.has(object); } private getReflectedType(target: Function, propertyName: string): Function | undefined { if (!target) return undefined; const meta = defaultMetadataStorage.findTypeMetadata(target, propertyName); return meta ? meta.reflectedType : undefined; } private getKeys(target: Function, object: Record, isMap: boolean): string[] { // determine exclusion strategy let strategy = defaultMetadataStorage.getStrategy(target); if (strategy === 'none') strategy = this.options.strategy || 'exposeAll'; // exposeAll is default strategy // get all keys that need to expose let keys: any[] = []; if (strategy === 'exposeAll' || isMap) { if (object instanceof Map) { keys = Array.from(object.keys()); } else { keys = Object.keys(object); } } if (isMap) { // expose & exclude do not apply for map keys only to fields return keys; } /** * If decorators are ignored but we don't want the extraneous values, then we use the * metadata to decide which property is needed, but doesn't apply the decorator effect. */ if (this.options.ignoreDecorators && this.options.excludeExtraneousValues && target) { const exposedProperties = defaultMetadataStorage.getExposedProperties(target, this.transformationType); const excludedProperties = defaultMetadataStorage.getExcludedProperties(target, this.transformationType); keys = [...exposedProperties, ...excludedProperties]; } if (!this.options.ignoreDecorators && target) { // add all exposed to list of keys let exposedProperties = defaultMetadataStorage.getExposedProperties(target, this.transformationType); if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { exposedProperties = exposedProperties.map(key => { const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key); if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name) { return exposeMetadata.options.name; } return key; }); } if (this.options.excludeExtraneousValues) { keys = exposedProperties; } else { keys = keys.concat(exposedProperties); } // exclude excluded properties const excludedProperties = defaultMetadataStorage.getExcludedProperties(target, this.transformationType); if (excludedProperties.length > 0) { keys = keys.filter(key => { return !excludedProperties.includes(key); }); } // apply versioning options if (this.options.version !== undefined) { keys = keys.filter(key => { const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key); if (!exposeMetadata || !exposeMetadata.options) return true; return this.checkVersion(exposeMetadata.options.since, exposeMetadata.options.until); }); } // apply grouping options if (this.options.groups && this.options.groups.length) { keys = keys.filter(key => { const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key); if (!exposeMetadata || !exposeMetadata.options) return true; return this.checkGroups(exposeMetadata.options.groups); }); } else { keys = keys.filter(key => { const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key); return ( !exposeMetadata || !exposeMetadata.options || !exposeMetadata.options.groups || !exposeMetadata.options.groups.length ); }); } } // exclude prefixed properties if (this.options.excludePrefixes && this.options.excludePrefixes.length) { keys = keys.filter(key => this.options.excludePrefixes.every(prefix => { return key.substr(0, prefix.length) !== prefix; }) ); } // make sure we have unique keys keys = keys.filter((key, index, self) => { return self.indexOf(key) === index; }); return keys; } private checkVersion(since: number, until: number): boolean { let decision = true; if (decision && since) decision = this.options.version >= since; if (decision && until) decision = this.options.version < until; return decision; } private checkGroups(groups: string[]): boolean { if (!groups) return true; return this.options.groups.some(optionGroup => groups.includes(optionGroup)); } private getPropertyDescriptor(obj: any, key: PropertyKey): PropertyDescriptor | undefined { const descriptor = Object.getOwnPropertyDescriptor(obj, key); if (descriptor) return descriptor; const prototype = Object.getPrototypeOf(obj); return prototype ? this.getPropertyDescriptor(prototype, key) : undefined; } } ================================================ FILE: src/constants/default-options.constant.ts ================================================ import { ClassTransformOptions } from '../interfaces/class-transformer-options.interface'; /** * These are the default options used by any transformation operation. */ export const defaultOptions: Partial = { enableCircularCheck: false, enableImplicitConversion: false, excludeExtraneousValues: false, excludePrefixes: undefined, exposeDefaultValues: false, exposeUnsetFields: true, groups: undefined, ignoreDecorators: false, strategy: undefined, targetMaps: undefined, version: undefined, }; ================================================ FILE: src/decorators/exclude.decorator.ts ================================================ import { defaultMetadataStorage } from '../storage'; import { ExcludeOptions } from '../interfaces'; /** * Marks the given class or property as excluded. By default the property is excluded in both * constructorToPlain and plainToConstructor transformations. It can be limited to only one direction * via using the `toPlainOnly` or `toClassOnly` option. * * Can be applied to class definitions and properties. */ export function Exclude(options: ExcludeOptions = {}): PropertyDecorator & ClassDecorator { /** * NOTE: The `propertyName` property must be marked as optional because * this decorator used both as a class and a property decorator and the * Typescript compiler will freak out if we make it mandatory as a class * decorator only receives one parameter. */ return function (object: any, propertyName?: string | Symbol): void { defaultMetadataStorage.addExcludeMetadata({ target: object instanceof Function ? object : object.constructor, propertyName: propertyName as string, options, }); }; } ================================================ FILE: src/decorators/expose.decorator.ts ================================================ import { defaultMetadataStorage } from '../storage'; import { ExposeOptions } from '../interfaces'; /** * Marks the given class or property as included. By default the property is included in both * constructorToPlain and plainToConstructor transformations. It can be limited to only one direction * via using the `toPlainOnly` or `toClassOnly` option. * * Can be applied to class definitions and properties. */ export function Expose(options: ExposeOptions = {}): PropertyDecorator & ClassDecorator { /** * NOTE: The `propertyName` property must be marked as optional because * this decorator used both as a class and a property decorator and the * Typescript compiler will freak out if we make it mandatory as a class * decorator only receives one parameter. */ return function (object: any, propertyName?: string | Symbol): void { defaultMetadataStorage.addExposeMetadata({ target: object instanceof Function ? object : object.constructor, propertyName: propertyName as string, options, }); }; } ================================================ FILE: src/decorators/index.ts ================================================ export * from './exclude.decorator'; export * from './expose.decorator'; export * from './transform-instance-to-instance.decorator'; export * from './transform-instance-to-plain.decorator'; export * from './transform-plain-to-instance.decorator'; export * from './transform.decorator'; export * from './type.decorator'; ================================================ FILE: src/decorators/transform-instance-to-instance.decorator.ts ================================================ import { ClassTransformer } from '../ClassTransformer'; import { ClassTransformOptions } from '../interfaces'; /** * Return the class instance only with the exposed properties. * * Can be applied to functions and getters/setters only. */ export function TransformInstanceToInstance(params?: ClassTransformOptions): MethodDecorator { return function (target: Record, propertyKey: string | Symbol, descriptor: PropertyDescriptor): void { const classTransformer: ClassTransformer = new ClassTransformer(); const originalMethod = descriptor.value; descriptor.value = function (...args: any[]): Record { const result: any = originalMethod.apply(this, args); const isPromise = !!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function'; return isPromise ? result.then((data: any) => classTransformer.instanceToInstance(data, params)) : classTransformer.instanceToInstance(result, params); }; }; } ================================================ FILE: src/decorators/transform-instance-to-plain.decorator.ts ================================================ import { ClassTransformer } from '../ClassTransformer'; import { ClassTransformOptions } from '../interfaces'; /** * Transform the object from class to plain object and return only with the exposed properties. * * Can be applied to functions and getters/setters only. */ export function TransformInstanceToPlain(params?: ClassTransformOptions): MethodDecorator { return function (target: Record, propertyKey: string | Symbol, descriptor: PropertyDescriptor): void { const classTransformer: ClassTransformer = new ClassTransformer(); const originalMethod = descriptor.value; descriptor.value = function (...args: any[]): Record { const result: any = originalMethod.apply(this, args); const isPromise = !!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function'; return isPromise ? result.then((data: any) => classTransformer.instanceToPlain(data, params)) : classTransformer.instanceToPlain(result, params); }; }; } ================================================ FILE: src/decorators/transform-plain-to-instance.decorator.ts ================================================ import { ClassTransformer } from '../ClassTransformer'; import { ClassTransformOptions, ClassConstructor } from '../interfaces'; /** * Return the class instance only with the exposed properties. * * Can be applied to functions and getters/setters only. */ export function TransformPlainToInstance( classType: ClassConstructor, params?: ClassTransformOptions ): MethodDecorator { return function (target: Record, propertyKey: string | Symbol, descriptor: PropertyDescriptor): void { const classTransformer: ClassTransformer = new ClassTransformer(); const originalMethod = descriptor.value; descriptor.value = function (...args: any[]): Record { const result: any = originalMethod.apply(this, args); const isPromise = !!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function'; return isPromise ? result.then((data: any) => classTransformer.plainToInstance(classType, data, params)) : classTransformer.plainToInstance(classType, result, params); }; }; } ================================================ FILE: src/decorators/transform.decorator.ts ================================================ import { defaultMetadataStorage } from '../storage'; import { TransformFnParams, TransformOptions } from '../interfaces'; /** * Defines a custom logic for value transformation. * * Can be applied to properties only. */ export function Transform( transformFn: (params: TransformFnParams) => any, options: TransformOptions = {} ): PropertyDecorator { return function (target: any, propertyName: string | Symbol): void { defaultMetadataStorage.addTransformMetadata({ target: target.constructor, propertyName: propertyName as string, transformFn, options, }); }; } ================================================ FILE: src/decorators/type.decorator.ts ================================================ import { defaultMetadataStorage } from '../storage'; import { TypeHelpOptions, TypeOptions } from '../interfaces'; /** * Specifies a type of the property. * The given TypeFunction can return a constructor. A discriminator can be given in the options. * * Can be applied to properties only. */ export function Type( typeFunction?: (type?: TypeHelpOptions) => Function, options: TypeOptions = {} ): PropertyDecorator { return function (target: any, propertyName: string | Symbol): void { const reflectedType = (Reflect as any).getMetadata('design:type', target, propertyName); defaultMetadataStorage.addTypeMetadata({ target: target.constructor, propertyName: propertyName as string, reflectedType, typeFunction, options, }); }; } ================================================ FILE: src/enums/index.ts ================================================ export * from './transformation-type.enum'; ================================================ FILE: src/enums/transformation-type.enum.ts ================================================ export enum TransformationType { PLAIN_TO_CLASS, CLASS_TO_PLAIN, CLASS_TO_CLASS, } ================================================ FILE: src/index.ts ================================================ import { ClassTransformer } from './ClassTransformer'; import { ClassTransformOptions } from './interfaces'; import { ClassConstructor } from './interfaces'; export { ClassTransformer } from './ClassTransformer'; export * from './decorators'; export * from './interfaces'; export * from './enums'; const classTransformer = new ClassTransformer(); /** * Converts class (constructor) object to plain (literal) object. Also works with arrays. * * @deprecated Function name changed, use the `instanceToPlain` method instead. */ export function classToPlain(object: T, options?: ClassTransformOptions): Record; export function classToPlain(object: T[], options?: ClassTransformOptions): Record[]; export function classToPlain( object: T | T[], options?: ClassTransformOptions ): Record | Record[] { return classTransformer.instanceToPlain(object, options); } /** * Converts class (constructor) object to plain (literal) object. Also works with arrays. */ export function instanceToPlain(object: T, options?: ClassTransformOptions): Record; export function instanceToPlain(object: T[], options?: ClassTransformOptions): Record[]; export function instanceToPlain( object: T | T[], options?: ClassTransformOptions ): Record | Record[] { return classTransformer.instanceToPlain(object, options); } /** * Converts class (constructor) object to plain (literal) object. * Uses given plain object as source object (it means fills given plain object with data from class object). * Also works with arrays. * * @deprecated This function is being removed. */ export function classToPlainFromExist( object: T, plainObject: Record, options?: ClassTransformOptions ): Record; export function classToPlainFromExist( object: T, plainObjects: Record[], options?: ClassTransformOptions ): Record[]; export function classToPlainFromExist( object: T, plainObject: Record | Record[], options?: ClassTransformOptions ): Record | Record[] { return classTransformer.classToPlainFromExist(object, plainObject, options); } /** * Converts plain (literal) object to class (constructor) object. Also works with arrays. * * @deprecated Function name changed, use the `plainToInstance` method instead. */ export function plainToClass(cls: ClassConstructor, plain: V[], options?: ClassTransformOptions): T[]; export function plainToClass(cls: ClassConstructor, plain: V, options?: ClassTransformOptions): T; export function plainToClass(cls: ClassConstructor, plain: V | V[], options?: ClassTransformOptions): T | T[] { return classTransformer.plainToInstance(cls, plain as any, options); } /** * Converts plain (literal) object to class (constructor) object. Also works with arrays. */ export function plainToInstance(cls: ClassConstructor, plain: V[], options?: ClassTransformOptions): T[]; export function plainToInstance(cls: ClassConstructor, plain: V, options?: ClassTransformOptions): T; export function plainToInstance( cls: ClassConstructor, plain: V | V[], options?: ClassTransformOptions ): T | T[] { return classTransformer.plainToInstance(cls, plain as any, options); } /** * Converts plain (literal) object to class (constructor) object. * Uses given object as source object (it means fills given object with data from plain object). * Also works with arrays. * * @deprecated This function is being removed. The current implementation is incorrect as it modifies the source object. */ export function plainToClassFromExist(clsObject: T[], plain: V[], options?: ClassTransformOptions): T[]; export function plainToClassFromExist(clsObject: T, plain: V, options?: ClassTransformOptions): T; export function plainToClassFromExist(clsObject: T, plain: V | V[], options?: ClassTransformOptions): T | T[] { return classTransformer.plainToClassFromExist(clsObject, plain, options); } /** * Converts class (constructor) object to new class (constructor) object. Also works with arrays. */ export function instanceToInstance(object: T, options?: ClassTransformOptions): T; export function instanceToInstance(object: T[], options?: ClassTransformOptions): T[]; export function instanceToInstance(object: T | T[], options?: ClassTransformOptions): T | T[] { return classTransformer.instanceToInstance(object, options); } /** * Converts class (constructor) object to plain (literal) object. * Uses given plain object as source object (it means fills given plain object with data from class object). * Also works with arrays. * * @deprecated This function is being removed. The current implementation is incorrect as it modifies the source object. */ export function classToClassFromExist(object: T, fromObject: T, options?: ClassTransformOptions): T; export function classToClassFromExist(object: T, fromObjects: T[], options?: ClassTransformOptions): T[]; export function classToClassFromExist(object: T, fromObject: T | T[], options?: ClassTransformOptions): T | T[] { return classTransformer.classToClassFromExist(object, fromObject, options); } /** * Serializes given object to a JSON string. * * @deprecated This function is being removed. Please use * ``` * JSON.stringify(instanceToPlain(object, options)) * ``` */ export function serialize(object: T, options?: ClassTransformOptions): string; export function serialize(object: T[], options?: ClassTransformOptions): string; export function serialize(object: T | T[], options?: ClassTransformOptions): string { return classTransformer.serialize(object, options); } /** * Deserializes given JSON string to a object of the given class. * * @deprecated This function is being removed. Please use the following instead: * ``` * instanceToClass(cls, JSON.parse(json), options) * ``` */ export function deserialize(cls: ClassConstructor, json: string, options?: ClassTransformOptions): T { return classTransformer.deserialize(cls, json, options); } /** * Deserializes given JSON string to an array of objects of the given class. * * @deprecated This function is being removed. Please use the following instead: * ``` * JSON.parse(json).map(value => instanceToClass(cls, value, options)) * ``` * */ export function deserializeArray(cls: ClassConstructor, json: string, options?: ClassTransformOptions): T[] { return classTransformer.deserializeArray(cls, json, options); } ================================================ FILE: src/interfaces/class-constructor.type.ts ================================================ export type ClassConstructor = { new (...args: any[]): T; }; ================================================ FILE: src/interfaces/class-transformer-options.interface.ts ================================================ import { TargetMap } from './target-map.interface'; /** * Options to be passed during transformation. */ export interface ClassTransformOptions { /** * Exclusion strategy. By default exposeAll is used, which means that it will expose all properties are transformed * by default. */ strategy?: 'excludeAll' | 'exposeAll'; /** * Indicates if extraneous properties should be excluded from the value when converting a plain value to a class. * * This option requires that each property on the target class has at least one `@Expose` or `@Exclude` decorator * assigned from this library. */ excludeExtraneousValues?: boolean; /** * Only properties with given groups gonna be transformed. */ groups?: string[]; /** * Only properties with "since" > version < "until" gonna be transformed. */ version?: number; /** * Excludes properties with the given prefixes. For example, if you mark your private properties with "_" and "__" * you can set this option's value to ["_", "__"] and all private properties will be skipped. * This works only for "exposeAll" strategy. */ excludePrefixes?: string[]; /** * If set to true then class transformer will ignore the effect of all @Expose and @Exclude decorators. * This option is useful if you want to kinda clone your object but do not apply decorators affects. * * __NOTE:__ You may still have to add the decorators to make other options work. */ ignoreDecorators?: boolean; /** * Target maps allows to set a Types of the transforming object without using @Type decorator. * This is useful when you are transforming external classes, or if you already have type metadata for * objects and you don't want to set it up again. */ targetMaps?: TargetMap[]; /** * If set to true then class transformer will perform a circular check. (circular check is turned off by default) * This option is useful when you know for sure that your types might have a circular dependency. */ enableCircularCheck?: boolean; /** * If set to true then class transformer will try to convert properties implicitly to their target type based on their typing information. * * DEFAULT: `false` */ enableImplicitConversion?: boolean; /** * If set to true then class transformer will take default values for unprovided fields. * This is useful when you convert a plain object to a class and have an optional field with a default value. */ exposeDefaultValues?: boolean; /** * When set to true, fields with `undefined` as value will be included in class to plain transformation. Otherwise * those fields will be omitted from the result. * * DEFAULT: `true` */ exposeUnsetFields?: boolean; } ================================================ FILE: src/interfaces/decorator-options/exclude-options.interface.ts ================================================ /** * Possible transformation options for the @Exclude decorator. */ export interface ExcludeOptions { /** * Exclude this property only when transforming from plain to class instance. */ toClassOnly?: boolean; /** * Exclude this property only when transforming from class instance to plain object. */ toPlainOnly?: boolean; } ================================================ FILE: src/interfaces/decorator-options/expose-options.interface.ts ================================================ /** * Possible transformation options for the @Expose decorator. */ export interface ExposeOptions { /** * Name of property on the target object to expose the value of this property. */ name?: string; /** * First version where this property should be exposed. * * Example: * ```ts * instanceToPlain(payload, { version: 1.0 }); * ``` */ since?: number; /** * Last version where this property should be exposed. * * Example: * ```ts * instanceToPlain(payload, { version: 1.0 }); * ``` */ until?: number; /** * List of transformation groups this property belongs to. When set, * the property will be exposed only when transform is called with * one of the groups specified. * * Example: * ```ts * instanceToPlain(payload, { groups: ['user'] }); * ``` */ groups?: string[]; /** * Expose this property only when transforming from plain to class instance. */ toClassOnly?: boolean; /** * Expose this property only when transforming from class instance to plain object. */ toPlainOnly?: boolean; } ================================================ FILE: src/interfaces/decorator-options/transform-options.interface.ts ================================================ /** * Possible transformation options for the @Transform decorator. */ export interface TransformOptions { /** * First version where this property should be exposed. * * Example: * ```ts * instanceToPlain(payload, { version: 1.0 }); * ``` */ since?: number; /** * Last version where this property should be exposed. * * Example: * ```ts * instanceToPlain(payload, { version: 1.0 }); * ``` */ until?: number; /** * List of transformation groups this property belongs to. When set, * the property will be exposed only when transform is called with * one of the groups specified. * * Example: * ```ts * instanceToPlain(payload, { groups: ['user'] }); * ``` */ groups?: string[]; /** * Expose this property only when transforming from plain to class instance. */ toClassOnly?: boolean; /** * Expose this property only when transforming from class instance to plain object. */ toPlainOnly?: boolean; } ================================================ FILE: src/interfaces/decorator-options/type-discriminator-descriptor.interface.ts ================================================ import { ClassConstructor } from '..'; /** * Discriminator object containing the type information to select a proper type * during transformation when a discriminator property is provided. */ export interface DiscriminatorDescriptor { /** * The name of the property which holds the type information in the received object. */ property: string; /** * List of the available types. The transformer will try to lookup the object * with the same key as the value received in the defined discriminator property * and create an instance of the defined class. */ subTypes: { /** * Name of the type. */ name: string | number; /** * A class constructor which can be used to create the object. */ value: ClassConstructor; }[]; } ================================================ FILE: src/interfaces/decorator-options/type-options.interface.ts ================================================ import { DiscriminatorDescriptor } from './type-discriminator-descriptor.interface'; /** * Possible transformation options for the @Type decorator. */ export interface TypeOptions { /** * Optional discriminator object, when provided the property value will be * initialized according to the specified object. */ discriminator?: DiscriminatorDescriptor; /** * Indicates whether to keep the discriminator property on the * transformed object or not. Disabled by default. * * @default false */ keepDiscriminatorProperty?: boolean; } ================================================ FILE: src/interfaces/index.ts ================================================ export * from './decorator-options/expose-options.interface'; export * from './decorator-options/exclude-options.interface'; export * from './decorator-options/transform-options.interface'; export * from './decorator-options/type-discriminator-descriptor.interface'; export * from './decorator-options/type-options.interface'; export * from './metadata/exclude-metadata.interface'; export * from './metadata/expose-metadata.interface'; export * from './metadata/transform-metadata.interface'; export * from './metadata/transform-fn-params.interface'; export * from './metadata/type-metadata.interface'; export * from './class-constructor.type'; export * from './class-transformer-options.interface'; export * from './target-map.interface'; export * from './type-help-options.interface'; ================================================ FILE: src/interfaces/metadata/exclude-metadata.interface.ts ================================================ import { ExcludeOptions } from '..'; /** * This object represents metadata assigned to a property via the @Exclude decorator. */ export interface ExcludeMetadata { target: Function; /** * The property name this metadata belongs to on the target (class or property). * * Note: If the decorator is applied to a class the propertyName will be undefined. */ propertyName: string | undefined; /** * Options passed to the @Exclude operator for this property. */ options: ExcludeOptions; } ================================================ FILE: src/interfaces/metadata/expose-metadata.interface.ts ================================================ import { ExposeOptions } from '..'; /** * This object represents metadata assigned to a property via the @Expose decorator. */ export interface ExposeMetadata { target: Function; /** * The property name this metadata belongs to on the target (class or property). * * Note: If the decorator is applied to a class the propertyName will be undefined. */ propertyName: string | undefined; /** * Options passed to the @Expose operator for this property. */ options: ExposeOptions; } ================================================ FILE: src/interfaces/metadata/transform-fn-params.interface.ts ================================================ import { TransformationType } from '../../enums'; import { ClassTransformOptions } from '../class-transformer-options.interface'; export interface TransformFnParams { value: any; key: string; obj: any; type: TransformationType; options: ClassTransformOptions; } ================================================ FILE: src/interfaces/metadata/transform-metadata.interface.ts ================================================ import { TransformOptions } from '..'; import { TransformFnParams } from './transform-fn-params.interface'; /** * This object represents metadata assigned to a property via the @Transform decorator. */ export interface TransformMetadata { target: Function; /** * The property name this metadata belongs to on the target (property only). */ propertyName: string; /** * The custom transformation function provided by the user in the @Transform decorator. */ transformFn: (params: TransformFnParams) => any; /** * Options passed to the @Transform operator for this property. */ options: TransformOptions; } ================================================ FILE: src/interfaces/metadata/type-metadata.interface.ts ================================================ import { TypeHelpOptions, TypeOptions } from '..'; /** * This object represents metadata assigned to a property via the @Type decorator. */ export interface TypeMetadata { target: Function; /** * The property name this metadata belongs to on the target (property only). */ propertyName: string; /** * The type guessed from assigned Reflect metadata ('design:type') */ reflectedType: any; /** * The custom function provided by the user in the @Type decorator which * returns the target type for the transformation. */ typeFunction: (options?: TypeHelpOptions) => Function; /** * Options passed to the @Type operator for this property. */ options: TypeOptions; } ================================================ FILE: src/interfaces/target-map.interface.ts ================================================ /** * Allows to specify a map of Types in the object without using @Type decorator. * This is useful when you have external classes. */ export interface TargetMap { /** * Target which Types are being specified. */ target: Function; /** * List of properties and their Types. */ properties: { [key: string]: Function }; } ================================================ FILE: src/interfaces/type-help-options.interface.ts ================================================ // TODO: Document this interface. What does each property means? export interface TypeHelpOptions { newObject: any; object: Record; property: string; } ================================================ FILE: src/storage.ts ================================================ import { MetadataStorage } from './MetadataStorage'; import { getGlobal } from './utils'; const globalScope = getGlobal(); /** * Default metadata storage is used as singleton and can be used to storage all metadatas. */ if (!globalScope.classTransformerMetadataStorage) { globalScope.classTransformerMetadataStorage = new MetadataStorage(); } export const defaultMetadataStorage = globalScope.classTransformerMetadataStorage; ================================================ FILE: src/utils/get-global.util.spect.ts ================================================ import { getGlobal } from '.'; describe('getGlobal()', () => { it('should return true if Buffer is present in globalThis', () => { expect(getGlobal().Buffer).toBe(true); }); it('should return false if Buffer is not present in globalThis', () => { const bufferImp = global.Buffer; delete global.Buffer; expect(getGlobal().Buffer).toBe(false); global.Buffer = bufferImp; }); }); ================================================ FILE: src/utils/get-global.util.ts ================================================ /** * This function returns the global object across Node and browsers. * * Note: `globalThis` is the standardized approach however it has been added to * Node.js in version 12. We need to include this snippet until Node 12 EOL. */ export function getGlobal() { if (typeof globalThis !== 'undefined') { return globalThis; } if (typeof global !== 'undefined') { return global; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Cannot find name 'window'. if (typeof window !== 'undefined') { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Cannot find name 'window'. return window; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Cannot find name 'self'. if (typeof self !== 'undefined') { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Cannot find name 'self'. return self; } } ================================================ FILE: src/utils/index.ts ================================================ export * from './get-global.util'; export * from './is-promise.util'; ================================================ FILE: src/utils/is-promise.util.ts ================================================ export function isPromise(p: any): p is Promise { return p !== null && typeof p === 'object' && typeof p.then === 'function'; } ================================================ FILE: test/functional/basic-functionality.spec.ts ================================================ import 'reflect-metadata'; import { instanceToInstance, classToClassFromExist, instanceToPlain, classToPlainFromExist, plainToInstance, plainToClassFromExist, } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; import { Exclude, Expose, Type, Transform } from '../../src/decorators'; describe('basic functionality', () => { it('should convert instance of the given object to plain javascript object and should expose all properties since its a default behaviour', () => { defaultMetadataStorage.clear(); class User { id: number; firstName: string; lastName: string; password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); const classToClassUser = instanceToInstance(user); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).toEqual(user); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); }); it('should exclude extraneous values if the excludeExtraneousValues option is set to true', () => { defaultMetadataStorage.clear(); class User { @Expose() id: number; @Expose() firstName: string; @Expose() lastName: string; } const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', age: 12, }; const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toHaveProperty('age'); expect(transformedUser.id).toBeUndefined(); const transformedUserWithoutExtra = plainToInstance(User, fromPlainUser, { excludeExtraneousValues: true }); expect(transformedUserWithoutExtra).toBeInstanceOf(User); expect(transformedUserWithoutExtra).not.toHaveProperty('age'); }); it('should exclude extraneous values if both excludeExtraneousValues and ignoreDecorators option is set to true', () => { // fixes https://github.com/typestack/class-transformer/issues/533 defaultMetadataStorage.clear(); class ExampleClass { @Exclude() public valueOne!: number; @Expose() public valueTwo!: number; } const transformationOptions = { ignoreDecorators: true, excludeExtraneousValues: true, }; const instance = plainToInstance( ExampleClass, { valueOne: 42, valueTwo: 42, extra: true, _otherExtra: true }, transformationOptions ); expect(instance).toBeInstanceOf(ExampleClass); expect(instance).toEqual({ valueOne: 42, valueTwo: 42 }); (instance as any).extraProp = 'not-needed'; expect(instanceToPlain(instance, transformationOptions)).toEqual({ valueOne: 42, valueTwo: 42, }); }); it('should exclude all objects marked with @Exclude() decorator', () => { defaultMetadataStorage.clear(); class User { id: number; firstName: string; lastName: string; @Exclude() password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); expect(plainUser.password).toBeUndefined(); const existUser = { id: 1, age: 27, password: 'yayayaya' }; const plainUser2 = classToPlainFromExist(user, existUser); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'yayayaya', }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassUser = instanceToInstance(user); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); }); it('should exclude all properties from object if whole class is marked with @Exclude() decorator', () => { defaultMetadataStorage.clear(); @Exclude() class User { id: number; firstName: string; lastName: string; password: string; } const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({}); expect(plainUser.firstName).toBeUndefined(); expect(plainUser.lastName).toBeUndefined(); expect(plainUser.password).toBeUndefined(); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({}); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, }); const classToClassUser = instanceToInstance(user); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({}); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, }); }); it('should exclude all properties from object if whole class is marked with @Exclude() decorator, but include properties marked with @Expose() decorator', () => { defaultMetadataStorage.clear(); @Exclude() class User { id: number; @Expose() firstName: string; @Expose() lastName: string; password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); expect(plainUser.password).toBeUndefined(); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassUser = instanceToInstance(user); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); }); it('should exclude all properties from object if its defined via transformation options, but include properties marked with @Expose() decorator', () => { defaultMetadataStorage.clear(); class User { id: number; @Expose() firstName: string; @Expose() lastName: string; password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user, { strategy: 'excludeAll' }); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); expect(plainUser.password).toBeUndefined(); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser, { strategy: 'excludeAll' }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser, { strategy: 'excludeAll' }); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser, { strategy: 'excludeAll' }); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassUser = instanceToInstance(user, { strategy: 'excludeAll' }); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser, { strategy: 'excludeAll' }); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); }); it('should expose all properties from object if its defined via transformation options, but exclude properties marked with @Exclude() decorator', () => { defaultMetadataStorage.clear(); class User { id: number; firstName: string; @Exclude() lastName: string; @Exclude() password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user, { strategy: 'exposeAll' }); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', }); expect(plainUser.lastName).toBeUndefined(); expect(plainUser.password).toBeUndefined(); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser, { strategy: 'exposeAll' }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser, { strategy: 'exposeAll' }); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: 'Umed', }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser, { strategy: 'exposeAll' }); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', }); const classToClassUser = instanceToInstance(user, { strategy: 'exposeAll' }); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({ firstName: 'Umed', }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser, { strategy: 'exposeAll' }); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', }); }); it('should convert values to specific types if they are set via @Type decorator', () => { defaultMetadataStorage.clear(); class User { id: number; @Type(type => String) firstName: string; @Type(type => String) lastName: string; @Type(type => Number) password: number; @Type(type => Boolean) isActive: boolean; @Type(type => Date) registrationDate: Date; @Type(type => String) lastVisitDate: string; @Type(type => Buffer) uuidBuffer: Buffer; @Type(type => String) nullableString?: null | string; @Type(type => Number) nullableNumber?: null | number; @Type(type => Boolean) nullableBoolean?: null | boolean; @Type(type => Date) nullableDate?: null | Date; @Type(type => Buffer) nullableBuffer?: null | Buffer; } const date = new Date(); const user = new User(); const uuid = Buffer.from('1234'); user.firstName = 321 as any; user.lastName = 123 as any; user.password = '123' as any; user.isActive = '1' as any; user.registrationDate = date.toString() as any; user.lastVisitDate = date as any; user.uuidBuffer = uuid as any; user.nullableString = null as any; user.nullableNumber = null as any; user.nullableBoolean = null as any; user.nullableDate = null as any; user.nullableBuffer = null as any; const fromPlainUser = { firstName: 321, lastName: 123, password: '123', isActive: '1', registrationDate: date.toString(), lastVisitDate: date, uuidBuffer: uuid, nullableString: null as null | string, nullableNumber: null as null | string, nullableBoolean: null as null | string, nullableDate: null as null | string, nullableBuffer: null as null | string, }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user, { strategy: 'exposeAll' }); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), uuidBuffer: uuid, nullableString: null, nullableNumber: null, nullableBoolean: null, nullableDate: null, nullableBuffer: null, }); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser, { strategy: 'exposeAll' }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), uuidBuffer: uuid, nullableString: null, nullableNumber: null, nullableBoolean: null, nullableDate: null, nullableBuffer: null, }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser, { strategy: 'exposeAll' }); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), uuidBuffer: uuid, nullableString: null, nullableNumber: null, nullableBoolean: null, nullableDate: null, nullableBuffer: null, }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser, { strategy: 'exposeAll' }); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), uuidBuffer: uuid, nullableString: null, nullableNumber: null, nullableBoolean: null, nullableDate: null, nullableBuffer: null, }); const classToClassUser = instanceToInstance(user, { strategy: 'exposeAll' }); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({ firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), uuidBuffer: uuid, nullableString: null, nullableNumber: null, nullableBoolean: null, nullableDate: null, nullableBuffer: null, }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser, { strategy: 'exposeAll' }); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), uuidBuffer: uuid, nullableString: null, nullableNumber: null, nullableBoolean: null, nullableDate: null, nullableBuffer: null, }); }); it('should transform nested objects too and make sure their decorators are used too', () => { defaultMetadataStorage.clear(); class Photo { id: number; name: string; @Exclude() filename: string; uploadDate: Date; } class User { firstName: string; lastName: string; @Exclude() password: string; photo: Photo; // type should be automatically guessed } const photo = new Photo(); photo.id = 1; photo.name = 'Me'; photo.filename = 'iam.jpg'; photo.uploadDate = new Date(); const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.photo = photo; const plainUser: any = instanceToPlain(user, { strategy: 'exposeAll' }); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser.photo).not.toBeInstanceOf(Photo); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', uploadDate: photo.uploadDate, }, }); expect(plainUser.password).toBeUndefined(); expect(plainUser.photo.filename).toBeUndefined(); expect(plainUser.photo.uploadDate).toEqual(photo.uploadDate); expect(plainUser.photo.uploadDate).not.toBe(photo.uploadDate); const existUser = { id: 1, age: 27, photo: { id: 2, description: 'photo' } }; const plainUser2: any = classToPlainFromExist(user, existUser, { strategy: 'exposeAll' }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2.photo).not.toBeInstanceOf(Photo); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', uploadDate: photo.uploadDate, description: 'photo', }, }); expect(plainUser2).toEqual(existUser); expect(plainUser2.password).toBeUndefined(); expect(plainUser2.photo.filename).toBeUndefined(); expect(plainUser2.photo.uploadDate).toEqual(photo.uploadDate); expect(plainUser2.photo.uploadDate).not.toBe(photo.uploadDate); }); it('should transform nested objects too and make sure given type is used instead of automatically guessed one', () => { defaultMetadataStorage.clear(); class Photo { id: number; name: string; @Exclude() filename: string; } class ExtendedPhoto implements Photo { id: number; @Exclude() name: string; filename: string; } class User { id: number; firstName: string; lastName: string; @Exclude() password: string; @Type(type => ExtendedPhoto) // force specific type photo: Photo; } const photo = new Photo(); photo.id = 1; photo.name = 'Me'; photo.filename = 'iam.jpg'; const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.photo = photo; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'iam.jpg', }, }); expect(plainUser.password).toBeUndefined(); expect(plainUser.photo.name).toBeUndefined(); }); it('should convert given plain object to class instance object', () => { defaultMetadataStorage.clear(); class Photo { id: number; name: string; @Exclude() filename: string; metadata: string; uploadDate: Date; } class User { id: number; firstName: string; lastName: string; @Exclude() password: string; @Type(type => Photo) photo: Photo; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.photo = new Photo(); user.photo.id = 1; user.photo.name = 'Me'; user.photo.filename = 'iam.jpg'; user.photo.uploadDate = new Date(); const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, name: 'Me', filename: 'iam.jpg', uploadDate: new Date(), }, }; const fromExistUser = new User(); fromExistUser.id = 1; const fromExistPhoto = new Photo(); fromExistPhoto.metadata = 'taken by Camera'; fromExistUser.photo = fromExistPhoto; const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser.photo).toBeInstanceOf(Photo); expect(transformedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', uploadDate: fromPlainUser.photo.uploadDate, }, }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser); expect(fromExistTransformedUser).toEqual(fromExistUser); expect(fromExistTransformedUser.photo).toEqual(fromExistPhoto); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', metadata: 'taken by Camera', uploadDate: fromPlainUser.photo.uploadDate, }, }); const classToClassUser = instanceToInstance(user); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser.photo).toBeInstanceOf(Photo); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).not.toEqual(user.photo); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', uploadDate: user.photo.uploadDate, }, }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser.photo).toBeInstanceOf(Photo); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).not.toEqual(user.photo); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', metadata: 'taken by Camera', uploadDate: user.photo.uploadDate, }, }); }); it('should expose only properties that match given group', () => { defaultMetadataStorage.clear(); class Photo { id: number; @Expose({ groups: ['user', 'guest'], }) filename: string; @Expose({ groups: ['admin'], }) status: number; metadata: string; } class User { id: number; firstName: string; @Expose({ groups: ['user', 'guest'], }) lastName: string; @Expose({ groups: ['user'], }) password: string; @Expose({ groups: ['admin'], }) isActive: boolean; @Type(type => Photo) photo: Photo; @Expose({ groups: ['admin'], }) @Type(type => Photo) photos: Photo[]; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.isActive = false; user.photo = new Photo(); user.photo.id = 1; user.photo.filename = 'myphoto.jpg'; user.photo.status = 1; user.photos = [user.photo]; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', isActive: false, photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }; const fromExistUser = new User(); fromExistUser.id = 1; fromExistUser.photo = new Photo(); fromExistUser.photo.metadata = 'taken by Camera'; const plainUser1: any = instanceToPlain(user); expect(plainUser1).not.toBeInstanceOf(User); expect(plainUser1).toEqual({ firstName: 'Umed', photo: { id: 1, }, }); expect(plainUser1.lastName).toBeUndefined(); expect(plainUser1.password).toBeUndefined(); expect(plainUser1.isActive).toBeUndefined(); const plainUser2: any = instanceToPlain(user, { groups: ['user'] }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, filename: 'myphoto.jpg', }, }); expect(plainUser2.isActive).toBeUndefined(); const transformedUser2 = plainToInstance(User, fromPlainUser, { groups: ['user'] }); expect(transformedUser2).toBeInstanceOf(User); expect(transformedUser2.photo).toBeInstanceOf(Photo); expect(transformedUser2).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, filename: 'myphoto.jpg', }, }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser, { groups: ['user'] }); expect(fromExistTransformedUser).toEqual(fromExistUser); expect(fromExistTransformedUser.photo).toEqual(fromExistUser.photo); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, metadata: 'taken by Camera', filename: 'myphoto.jpg', }, }); const classToClassUser = instanceToInstance(user, { groups: ['user'] }); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser.photo).toBeInstanceOf(Photo); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).not.toEqual(user.photo); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, filename: 'myphoto.jpg', }, }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser, { groups: ['user'] }); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser.photo).toBeInstanceOf(Photo); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).not.toEqual(user.photo); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, metadata: 'taken by Camera', filename: 'myphoto.jpg', }, }); const plainUser3: any = instanceToPlain(user, { groups: ['guest'] }); expect(plainUser3).not.toBeInstanceOf(User); expect(plainUser3).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', }, }); expect(plainUser3.password).toBeUndefined(); expect(plainUser3.isActive).toBeUndefined(); const transformedUser3 = plainToInstance(User, fromPlainUser, { groups: ['guest'] }); expect(transformedUser3).toBeInstanceOf(User); expect(transformedUser3.photo).toBeInstanceOf(Photo); expect(transformedUser3).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', }, }); const plainUser4: any = instanceToPlain(user, { groups: ['admin'] }); expect(plainUser4).not.toBeInstanceOf(User); expect(plainUser4).toEqual({ firstName: 'Umed', isActive: false, photo: { id: 1, status: 1, }, photos: [ { id: 1, status: 1, }, ], }); expect(plainUser4.lastName).toBeUndefined(); expect(plainUser4.password).toBeUndefined(); const transformedUser4 = plainToInstance(User, fromPlainUser, { groups: ['admin'] }); expect(transformedUser4).toBeInstanceOf(User); expect(transformedUser4.photo).toBeInstanceOf(Photo); expect(transformedUser4.photos[0]).toBeInstanceOf(Photo); expect(transformedUser4).toEqual({ firstName: 'Umed', isActive: false, photo: { id: 1, status: 1, }, photos: [ { id: 1, status: 1, }, ], }); const plainUser5: any = instanceToPlain(user, { groups: ['admin', 'user'] }); expect(plainUser5).not.toBeInstanceOf(User); expect(plainUser5).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', isActive: false, photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }); const transformedUser5 = plainToInstance(User, fromPlainUser, { groups: ['admin', 'user'] }); expect(transformedUser5).toBeInstanceOf(User); expect(transformedUser5).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', isActive: false, photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }); }); it('should expose only properties that match given version', () => { defaultMetadataStorage.clear(); class Photo { id: number; @Expose({ since: 1.5, until: 2, }) filename: string; @Expose({ since: 2, }) status: number; } class User { @Expose({ since: 1, until: 2, }) firstName: string; @Expose({ since: 0.5, }) lastName: string; @Exclude() password: string; @Type(type => Photo) photo: Photo; @Expose({ since: 3, }) @Type(type => Photo) photos: Photo[]; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.photo = new Photo(); user.photo.id = 1; user.photo.filename = 'myphoto.jpg'; user.photo.status = 1; user.photos = [user.photo]; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }; const plainUser1: any = instanceToPlain(user); expect(plainUser1).not.toBeInstanceOf(User); expect(plainUser1).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }); const transformedUser1 = plainToInstance(User, fromPlainUser); expect(transformedUser1).toBeInstanceOf(User); expect(transformedUser1.photo).toBeInstanceOf(Photo); expect(transformedUser1.photos[0]).toBeInstanceOf(Photo); expect(transformedUser1).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }); const plainUser2: any = instanceToPlain(user, { version: 0.3 }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ photo: { id: 1, }, }); const transformedUser2 = plainToInstance(User, fromPlainUser, { version: 0.3 }); expect(transformedUser2).toBeInstanceOf(User); expect(transformedUser2.photo).toBeInstanceOf(Photo); expect(transformedUser2).toEqual({ photo: { id: 1, }, }); const plainUser3: any = instanceToPlain(user, { version: 0.5 }); expect(plainUser3).not.toBeInstanceOf(User); expect(plainUser3).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, }, }); const transformedUser3 = plainToInstance(User, fromPlainUser, { version: 0.5 }); expect(transformedUser3).toBeInstanceOf(User); expect(transformedUser3.photo).toBeInstanceOf(Photo); expect(transformedUser3).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, }, }); const plainUser4: any = instanceToPlain(user, { version: 1 }); expect(plainUser4).not.toBeInstanceOf(User); expect(plainUser4).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, }, }); const transformedUser4 = plainToInstance(User, fromPlainUser, { version: 1 }); expect(transformedUser4).toBeInstanceOf(User); expect(transformedUser4.photo).toBeInstanceOf(Photo); expect(transformedUser4).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, }, }); const plainUser5: any = instanceToPlain(user, { version: 1.5 }); expect(plainUser5).not.toBeInstanceOf(User); expect(plainUser5).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', }, }); const transformedUser5 = plainToInstance(User, fromPlainUser, { version: 1.5 }); expect(transformedUser5).toBeInstanceOf(User); expect(transformedUser5.photo).toBeInstanceOf(Photo); expect(transformedUser5).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', }, }); const plainUser6: any = instanceToPlain(user, { version: 2 }); expect(plainUser6).not.toBeInstanceOf(User); expect(plainUser6).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, status: 1, }, }); const transformedUser6 = plainToInstance(User, fromPlainUser, { version: 2 }); expect(transformedUser6).toBeInstanceOf(User); expect(transformedUser6.photo).toBeInstanceOf(Photo); expect(transformedUser6).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, status: 1, }, }); const plainUser7: any = instanceToPlain(user, { version: 3 }); expect(plainUser7).not.toBeInstanceOf(User); expect(plainUser7).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, status: 1, }, photos: [ { id: 1, status: 1, }, ], }); const transformedUser7 = plainToInstance(User, fromPlainUser, { version: 3 }); expect(transformedUser7).toBeInstanceOf(User); expect(transformedUser7.photo).toBeInstanceOf(Photo); expect(transformedUser7.photos[0]).toBeInstanceOf(Photo); expect(transformedUser7).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, status: 1, }, photos: [ { id: 1, status: 1, }, ], }); }); it('should expose method and accessors that have @Expose()', () => { defaultMetadataStorage.clear(); class User { firstName: string; lastName: string; @Exclude() password: string; @Expose() get name(): string { return this.firstName + ' ' + this.lastName; } @Expose() getName(): string { return this.firstName + ' ' + this.lastName; } } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', name: 'Umed Khudoiberdiev', getName: 'Umed Khudoiberdiev', }); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); const likeUser = new User(); likeUser.firstName = 'Umed'; likeUser.lastName = 'Khudoiberdiev'; expect(transformedUser).toEqual(likeUser); }); it('should expose with alternative name if its given', () => { defaultMetadataStorage.clear(); class User { @Expose({ name: 'myName' }) firstName: string; @Expose({ name: 'secondName' }) lastName: string; @Exclude() password: string; @Expose() get name(): string { return this.firstName + ' ' + this.lastName; } @Expose({ name: 'fullName' }) getName(): string { return this.firstName + ' ' + this.lastName; } } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { myName: 'Umed', secondName: 'Khudoiberdiev', password: 'imnosuperman', }; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ myName: 'Umed', secondName: 'Khudoiberdiev', name: 'Umed Khudoiberdiev', fullName: 'Umed Khudoiberdiev', }); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); const likeUser = new User(); likeUser.firstName = 'Umed'; likeUser.lastName = 'Khudoiberdiev'; expect(transformedUser).toEqual(likeUser); }); it('should exclude all prefixed properties if prefix is given', () => { defaultMetadataStorage.clear(); class Photo { id: number; $filename: string; status: number; } class User { $system: string; _firstName: string; _lastName: string; @Exclude() password: string; @Type(() => Photo) photo: Photo; @Expose() get name(): string { return this._firstName + ' ' + this._lastName; } } const user = new User(); user.$system = '@#$%^&*token(*&^%$#@!'; user._firstName = 'Umed'; user._lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.photo = new Photo(); user.photo.id = 1; user.photo.$filename = 'myphoto.jpg'; user.photo.status = 1; const fromPlainUser = { $system: '@#$%^&*token(*&^%$#@!', _firstName: 'Khudoiberdiev', _lastName: 'imnosuperman', password: 'imnosuperman', photo: { id: 1, $filename: 'myphoto.jpg', status: 1, }, }; const plainUser: any = instanceToPlain(user, { excludePrefixes: ['_', '$'] }); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ name: 'Umed Khudoiberdiev', photo: { id: 1, status: 1, }, }); const transformedUser = plainToInstance(User, fromPlainUser, { excludePrefixes: ['_', '$'] }); expect(transformedUser).toBeInstanceOf(User); const likeUser = new User(); likeUser.photo = new Photo(); likeUser.photo.id = 1; likeUser.photo.status = 1; expect(transformedUser).toEqual(likeUser); }); it('should expose inherited method and accessors that have @Expose()', () => { class User { firstName: string; lastName: string; @Expose() get name() { return this.firstName + ' ' + this.lastName; } @Expose() getName() { return this.firstName + ' ' + this.lastName; } } class Programmer extends User { language: string; } const programmer = new Programmer(); programmer.firstName = 'Umed'; programmer.lastName = 'Khudoiberdiev'; programmer.language = 'en'; const fromPlainProgrammer = { firstName: 'Umed', lastName: 'Khudoiberdiev', language: 'en', }; const plainProgrammer: any = instanceToPlain(programmer); expect(plainProgrammer).not.toBeInstanceOf(Programmer); expect(plainProgrammer).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', language: 'en', name: 'Umed Khudoiberdiev', getName: 'Umed Khudoiberdiev', }); const transformedProgrammer = plainToInstance(Programmer, fromPlainProgrammer); expect(transformedProgrammer).toBeInstanceOf(Programmer); const likeProgrammer = new Programmer(); likeProgrammer.firstName = 'Umed'; likeProgrammer.lastName = 'Khudoiberdiev'; likeProgrammer.language = 'en'; expect(transformedProgrammer).toEqual(likeProgrammer); }); it('should transform array', () => { defaultMetadataStorage.clear(); class User { id: number; firstName: string; lastName: string; @Exclude() password: string; @Expose() get name(): string { return this.firstName + ' ' + this.lastName; } } const user1 = new User(); user1.firstName = 'Umed'; user1.lastName = 'Khudoiberdiev'; user1.password = 'imnosuperman'; const user2 = new User(); user2.firstName = 'Dima'; user2.lastName = 'Zotov'; user2.password = 'imnomesser'; const users = [user1, user2]; const plainUsers: any = instanceToPlain(users); expect(plainUsers).toEqual([ { firstName: 'Umed', lastName: 'Khudoiberdiev', name: 'Umed Khudoiberdiev', }, { firstName: 'Dima', lastName: 'Zotov', name: 'Dima Zotov', }, ]); const fromPlainUsers = [ { firstName: 'Umed', lastName: 'Khudoiberdiev', name: 'Umed Khudoiberdiev', }, { firstName: 'Dima', lastName: 'Zotov', name: 'Dima Zotov', }, ]; const existUsers = [ { id: 1, age: 27 }, { id: 2, age: 30 }, ]; const plainUser2 = classToPlainFromExist(users, existUsers); expect(plainUser2).toEqual([ { id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', name: 'Umed Khudoiberdiev', }, { id: 2, age: 30, firstName: 'Dima', lastName: 'Zotov', name: 'Dima Zotov', }, ]); const transformedUser = plainToInstance(User, fromPlainUsers); expect(transformedUser[0]).toBeInstanceOf(User); expect(transformedUser[1]).toBeInstanceOf(User); const likeUser1 = new User(); likeUser1.firstName = 'Umed'; likeUser1.lastName = 'Khudoiberdiev'; const likeUser2 = new User(); likeUser2.firstName = 'Dima'; likeUser2.lastName = 'Zotov'; expect(transformedUser).toEqual([likeUser1, likeUser2]); const classToClassUsers = instanceToInstance(users); expect(classToClassUsers[0]).toBeInstanceOf(User); expect(classToClassUsers[1]).toBeInstanceOf(User); expect(classToClassUsers[0]).not.toEqual(user1); expect(classToClassUsers[1]).not.toEqual(user1); const classUserLike1 = new User(); classUserLike1.firstName = 'Umed'; classUserLike1.lastName = 'Khudoiberdiev'; const classUserLike2 = new User(); classUserLike2.firstName = 'Dima'; classUserLike2.lastName = 'Zotov'; expect(classToClassUsers).toEqual([classUserLike1, classUserLike2]); const fromExistUser1 = new User(); fromExistUser1.id = 1; const fromExistUser2 = new User(); fromExistUser2.id = 2; const fromExistUsers = [fromExistUser1, fromExistUser2]; const classToClassFromExistUser = classToClassFromExist(users, fromExistUsers); expect(classToClassFromExistUser[0]).toBeInstanceOf(User); expect(classToClassFromExistUser[1]).toBeInstanceOf(User); expect(classToClassFromExistUser[0]).not.toEqual(user1); expect(classToClassFromExistUser[1]).not.toEqual(user1); expect(classToClassFromExistUser).toEqual(fromExistUsers); const fromExistUserLike1 = new User(); fromExistUserLike1.id = 1; fromExistUserLike1.firstName = 'Umed'; fromExistUserLike1.lastName = 'Khudoiberdiev'; const fromExistUserLike2 = new User(); fromExistUserLike2.id = 2; fromExistUserLike2.firstName = 'Dima'; fromExistUserLike2.lastName = 'Zotov'; expect(classToClassFromExistUser).toEqual([fromExistUserLike1, fromExistUserLike2]); }); it('should transform objects with null prototype', () => { class TestClass { prop: string; } const obj = Object.create(null); obj.a = 'JS FTW'; const transformedClass = plainToInstance(TestClass, obj); expect(transformedClass).toBeInstanceOf(TestClass); }); it('should not pollute the prototype with a `__proto__` property', () => { const object = JSON.parse('{"__proto__": { "admin": true }}'); const plainObject = {}; classToPlainFromExist(object, plainObject); expect((plainObject as any).admin).toEqual(undefined); }); it('should not pollute the prototype with a `constructor.prototype` property', () => { const object = JSON.parse('{"constructor": { "prototype": { "admin": true }}}'); const plainObject = {}; classToPlainFromExist(object, plainObject); expect((plainObject as any).admin).toEqual(undefined); }); it('should default union types where the plain type is an array to an array result', () => { class User { name: string; } class TestClass { @Type(() => User) usersDefined: User[] | undefined; @Type(() => User) usersUndefined: User[] | undefined; } const obj = Object.create(null); obj.usersDefined = [{ name: 'a-name' }]; obj.usersUndefined = undefined; const transformedClass = plainToInstance(TestClass, obj as Record); expect(transformedClass).toBeInstanceOf(TestClass); expect(transformedClass.usersDefined).toBeInstanceOf(Array); expect(transformedClass.usersDefined.length).toEqual(1); expect(transformedClass.usersDefined[0]).toBeInstanceOf(User); expect(transformedClass.usersDefined[0].name).toEqual('a-name'); expect(transformedClass.usersUndefined).toBeUndefined(); }); }); ================================================ FILE: test/functional/circular-reference-problem.spec.ts ================================================ import 'reflect-metadata'; import { instanceToInstance, instanceToPlain, plainToInstance } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; import { TransformOperationExecutor } from '../../src/TransformOperationExecutor'; describe('circular reference problem', () => { it('should skip circular reference objects in instanceToPlain operation', () => { defaultMetadataStorage.clear(); class Caption { text: string; } class Photo { id: number; filename: string; user: User; users: User[]; caption: Caption; } class User { id: number; firstName: string; caption: Caption; photos: Photo[]; } const photo1 = new Photo(); photo1.id = 1; photo1.filename = 'me.jpg'; const photo2 = new Photo(); photo2.id = 2; photo2.filename = 'she.jpg'; const caption = new Caption(); caption.text = 'cool photo'; const user = new User(); user.caption = caption; user.firstName = 'Umed Khudoiberdiev'; user.photos = [photo1, photo2]; photo1.user = user; photo2.user = user; photo1.users = [user]; photo2.users = [user]; photo1.caption = caption; photo2.caption = caption; const plainUser = instanceToPlain(user, { enableCircularCheck: true }); expect(plainUser).toEqual({ firstName: 'Umed Khudoiberdiev', caption: { text: 'cool photo' }, photos: [ { id: 1, filename: 'me.jpg', users: [], caption: { text: 'cool photo' }, }, { id: 2, filename: 'she.jpg', users: [], caption: { text: 'cool photo' }, }, ], }); }); it('should not skip circular reference objects, but handle it correctly in instanceToInstance operation', () => { defaultMetadataStorage.clear(); class Photo { id: number; filename: string; user: User; users: User[]; } class User { id: number; firstName: string; photos: Photo[]; } const photo1 = new Photo(); photo1.id = 1; photo1.filename = 'me.jpg'; const photo2 = new Photo(); photo2.id = 2; photo2.filename = 'she.jpg'; const user = new User(); user.firstName = 'Umed Khudoiberdiev'; user.photos = [photo1, photo2]; photo1.user = user; photo2.user = user; photo1.users = [user]; photo2.users = [user]; const classUser = instanceToInstance(user, { enableCircularCheck: true }); expect(classUser).not.toBe(user); expect(classUser).toBeInstanceOf(User); expect(classUser).toEqual(user); }); describe('enableCircularCheck option', () => { class Photo { id: number; filename: string; } class User { id: number; firstName: string; photos: Photo[]; } let isCircularSpy: jest.SpyInstance; const photo1 = new Photo(); photo1.id = 1; photo1.filename = 'me.jpg'; const user = new User(); user.firstName = 'Umed Khudoiberdiev'; user.photos = [photo1]; beforeEach(() => { isCircularSpy = jest.spyOn(TransformOperationExecutor.prototype, 'isCircular' as any); }); afterEach(() => { isCircularSpy.mockRestore(); }); it('enableCircularCheck option is undefined (default)', () => { plainToInstance>(User, user); expect(isCircularSpy).not.toHaveBeenCalled(); }); it('enableCircularCheck option is true', () => { plainToInstance>(User, user, { enableCircularCheck: true }); expect(isCircularSpy).toHaveBeenCalled(); }); }); }); ================================================ FILE: test/functional/custom-transform.spec.ts ================================================ /* eslint-disable @typescript-eslint/camelcase */ import 'reflect-metadata'; import { instanceToInstance, instanceToPlain, ClassTransformOptions, plainToInstance, TransformFnParams, } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; import { Expose, Transform, Type } from '../../src/decorators'; import { TransformationType } from '../../src/enums'; describe('custom transformation decorator', () => { it('@Expose decorator with "name" option should work with @Transform decorator', () => { defaultMetadataStorage.clear(); class User { @Expose({ name: 'user_name' }) @Transform(({ value }) => value.toUpperCase()) name: string; } const plainUser = { user_name: 'Johny Cage', }; const classedUser = plainToInstance(User, plainUser); expect(classedUser.name).toEqual('JOHNY CAGE'); }); it('@Transform decorator logic should be executed depend of toPlainOnly and toClassOnly set', () => { defaultMetadataStorage.clear(); class User { id: number; name: string; @Transform(({ value }) => value.toString(), { toPlainOnly: true }) @Transform(({ value }) => 'custom-transformed', { toClassOnly: true }) date: Date; } const plainUser = { id: 1, name: 'Johny Cage', date: new Date().valueOf(), }; const user = new User(); user.id = 1; user.name = 'Johny Cage'; user.date = new Date(); const classedUser = plainToInstance(User, plainUser); expect(classedUser).toBeInstanceOf(User); expect(classedUser.id).toEqual(1); expect(classedUser.name).toEqual('Johny Cage'); expect(classedUser.date).toBe('custom-transformed'); const plainedUser = instanceToPlain(user); expect(plainedUser).not.toBeInstanceOf(User); expect(plainedUser).toEqual({ id: 1, name: 'Johny Cage', date: user.date.toString(), }); }); it('versions and groups should work with @Transform decorator too', () => { defaultMetadataStorage.clear(); class User { id: number; name: string; @Type(() => Date) @Transform(({ value }) => 'custom-transformed-version-check', { since: 1, until: 2 }) date: Date; @Type(() => Date) @Transform(({ value }) => value.toString(), { groups: ['user'] }) lastVisitDate: Date; } const plainUser = { id: 1, name: 'Johny Cage', date: new Date().valueOf(), lastVisitDate: new Date().valueOf(), }; const classedUser1 = plainToInstance(User, plainUser); expect(classedUser1).toBeInstanceOf(User); expect(classedUser1.id).toEqual(1); expect(classedUser1.name).toEqual('Johny Cage'); expect(classedUser1.date).toBe('custom-transformed-version-check'); const classedUser2 = plainToInstance(User, plainUser, { version: 0.5 }); expect(classedUser2).toBeInstanceOf(User); expect(classedUser2.id).toEqual(1); expect(classedUser2.name).toEqual('Johny Cage'); expect(classedUser2.date).toBeInstanceOf(Date); const classedUser3 = plainToInstance(User, plainUser, { version: 1 }); expect(classedUser3).toBeInstanceOf(User); expect(classedUser3.id).toEqual(1); expect(classedUser3.name).toEqual('Johny Cage'); expect(classedUser3.date).toBe('custom-transformed-version-check'); const classedUser4 = plainToInstance(User, plainUser, { version: 2 }); expect(classedUser4).toBeInstanceOf(User); expect(classedUser4.id).toEqual(1); expect(classedUser4.name).toEqual('Johny Cage'); expect(classedUser4.date).toBeInstanceOf(Date); const classedUser5 = plainToInstance(User, plainUser, { groups: ['user'] }); expect(classedUser5).toBeInstanceOf(User); expect(classedUser5.id).toEqual(1); expect(classedUser5.name).toEqual('Johny Cage'); expect(classedUser5.lastVisitDate).toEqual(new Date(plainUser.lastVisitDate).toString()); }); it('@Transform decorator callback should be given correct arguments', () => { defaultMetadataStorage.clear(); let keyArg: string; let objArg: any; let typeArg: TransformationType; let optionsArg: ClassTransformOptions; function transformCallback({ value, key, obj, type, options }: TransformFnParams): any { keyArg = key; objArg = obj; typeArg = type; optionsArg = options; return value; } class User { @Transform(transformCallback, { toPlainOnly: true }) @Transform(transformCallback, { toClassOnly: true }) name: string; } const plainUser = { name: 'Johny Cage', }; const options: ClassTransformOptions = { groups: ['user', 'user.email'], version: 2, }; plainToInstance(User, plainUser, options); expect(keyArg).toBe('name'); expect(objArg).toEqual(plainUser); expect(typeArg).toEqual(TransformationType.PLAIN_TO_CLASS); expect(optionsArg.groups).toBe(options.groups); expect(optionsArg.version).toBe(options.version); const user = new User(); user.name = 'Johny Cage'; optionsArg = undefined; instanceToPlain(user, options); expect(keyArg).toBe('name'); expect(objArg).toEqual(user); expect(typeArg).toEqual(TransformationType.CLASS_TO_PLAIN); expect(optionsArg.groups).toBe(options.groups); expect(optionsArg.version).toBe(options.version); }); let model: any; it('should serialize json into model instance of class Person', () => { defaultMetadataStorage.clear(); expect(() => { const json = { name: 'John Doe', address: { street: 'Main Street 25', tel: '5454-534-645', zip: 10353, country: 'West Samoa', }, age: 25, hobbies: [ { type: 'sport', name: 'sailing' }, { type: 'relax', name: 'reading' }, { type: 'sport', name: 'jogging' }, { type: 'relax', name: 'movies' }, ], }; class Hobby { public type: string; public name: string; } class Address { public street: string; @Expose({ name: 'tel' }) public telephone: string; public zip: number; public country: string; } class Person { public name: string; @Type(() => Address) public address: Address; @Type(() => Hobby) @Transform(({ value }) => value.filter((hobby: any) => hobby.type === 'sport'), { toClassOnly: true }) public hobbies: Hobby[]; public age: number; } model = plainToInstance(Person, json); expect(model instanceof Person); expect(model.address instanceof Address); model.hobbies.forEach((hobby: Hobby) => expect(hobby instanceof Hobby && hobby.type === 'sport')); }).not.toThrow(); }); it('should serialize json into model instance of class Person with different possibilities for type of one property (polymorphism)', () => { defaultMetadataStorage.clear(); expect(() => { const json = { name: 'John Doe', hobby: { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, }; abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [ { value: Sports, name: 'sports' }, { value: Relaxing, name: 'relax' }, { value: Programming, name: 'program' }, ], }, }) public hobby: any; } const expectedHobby = { name: 'typescript coding', specialAbility: 'TESTING' }; const model: Person = plainToInstance(Person, json); expect(model).toBeInstanceOf(Person); expect(model.hobby).toBeInstanceOf(Programming); expect(model.hobby).not.toHaveProperty('__type'); expect(model.hobby).toHaveProperty('specialAbility', 'TESTING'); }).not.toThrow(); }); it('should serialize json into model instance of class Person with different types in array (polymorphism)', () => { defaultMetadataStorage.clear(); expect(() => { const json = { name: 'John Doe', hobbies: [ { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, { __type: 'relax', name: 'sun' }, ], }; abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [ { value: Sports, name: 'sports' }, { value: Relaxing, name: 'relax' }, { value: Programming, name: 'program' }, ], }, }) public hobbies: any[]; } const model: Person = plainToInstance(Person, json); expect(model).toBeInstanceOf(Person); expect(model.hobbies[0]).toBeInstanceOf(Programming); expect(model.hobbies[1]).toBeInstanceOf(Relaxing); expect(model.hobbies[0]).not.toHaveProperty('__type'); expect(model.hobbies[1]).not.toHaveProperty('__type'); expect(model.hobbies[1]).toHaveProperty('name', 'sun'); expect(model.hobbies[0]).toHaveProperty('specialAbility', 'TESTING'); }).not.toThrow(); }); it('should serialize json into model instance of class Person with different possibilities for type of one property AND keeps discriminator property (polymorphism)', () => { defaultMetadataStorage.clear(); expect(() => { const json = { name: 'John Doe', hobby: { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, }; abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [ { value: Sports, name: 'sports' }, { value: Relaxing, name: 'relax' }, { value: Programming, name: 'program' }, ], }, keepDiscriminatorProperty: true, }) public hobby: any; } const model: Person = plainToInstance(Person, json); expect(model).toBeInstanceOf(Person); expect(model.hobby).toBeInstanceOf(Programming); expect(model.hobby).toHaveProperty('__type'); expect(model.hobby).toHaveProperty('specialAbility', 'TESTING'); }).not.toThrow(); }); it('should serialize json into model instance of class Person with different types in array AND keeps discriminator property (polymorphism)', () => { defaultMetadataStorage.clear(); expect(() => { const json = { name: 'John Doe', hobbies: [ { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, { __type: 'relax', name: 'sun' }, ], }; abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [ { value: Sports, name: 'sports' }, { value: Relaxing, name: 'relax' }, { value: Programming, name: 'program' }, ], }, keepDiscriminatorProperty: true, }) public hobbies: any[]; } const model: Person = plainToInstance(Person, json); expect(model).toBeInstanceOf(Person); expect(model.hobbies[0]).toBeInstanceOf(Programming); expect(model.hobbies[1]).toBeInstanceOf(Relaxing); expect(model.hobbies[0]).toHaveProperty('__type'); expect(model.hobbies[1]).toHaveProperty('__type'); expect(model.hobbies[1]).toHaveProperty('name', 'sun'); expect(model.hobbies[0]).toHaveProperty('specialAbility', 'TESTING'); }).not.toThrow(); }); it('should deserialize class Person into json with different possibilities for type of one property (polymorphism)', () => { defaultMetadataStorage.clear(); expect(() => { abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [ { value: Sports, name: 'sports' }, { value: Relaxing, name: 'relax' }, { value: Programming, name: 'program' }, ], }, }) public hobby: any; } const model: Person = new Person(); const program = new Programming(); program.name = 'typescript coding'; program.specialAbility = 'testing'; model.name = 'John Doe'; model.hobby = program; const json: any = instanceToPlain(model); expect(json).not.toBeInstanceOf(Person); expect(json.hobby).toHaveProperty('__type', 'program'); }).not.toThrow(); }); it('should deserialize class Person into json with different types in array (polymorphism)', () => { defaultMetadataStorage.clear(); expect(() => { abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [ { value: Sports, name: 'sports' }, { value: Relaxing, name: 'relax' }, { value: Programming, name: 'program' }, ], }, }) public hobbies: any[]; } const model: Person = new Person(); const sport = new Sports(); sport.name = 'Football'; const program = new Programming(); program.name = 'typescript coding'; program.specialAbility = 'testing'; model.name = 'John Doe'; model.hobbies = [sport, program]; const json: any = instanceToPlain(model); expect(json).not.toBeInstanceOf(Person); expect(json.hobbies[0]).toHaveProperty('__type', 'sports'); expect(json.hobbies[1]).toHaveProperty('__type', 'program'); }).not.toThrow(); }); /** * test-case for issue #520 */ it('should deserialize undefined union type to undefined', () => { defaultMetadataStorage.clear(); expect(() => { abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [ { value: Sports, name: 'sports' }, { value: Relaxing, name: 'relax' }, { value: Programming, name: 'program' }, ], }, }) public hobby: Hobby; } const model: Person = new Person(); const sport = new Sports(); sport.name = 'Football'; const program = new Programming(); program.name = 'typescript coding'; program.specialAbility = 'testing'; model.name = 'John Doe'; // NOTE: hobby remains undefined model.hobby = undefined; const json: any = instanceToPlain(model); expect(json).not.toBeInstanceOf(Person); expect(json.hobby).toBeUndefined(); }).not.toThrow(); }); it('should transform class Person into class OtherPerson with different possibilities for type of one property (polymorphism)', () => { defaultMetadataStorage.clear(); expect(() => { abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [ { value: Sports, name: 'sports' }, { value: Relaxing, name: 'relax' }, { value: Programming, name: 'program' }, ], }, }) public hobby: any; } const model: Person = new Person(); const program = new Programming(); program.name = 'typescript coding'; program.specialAbility = 'testing'; model.name = 'John Doe'; model.hobby = program; const person: Person = instanceToInstance(model); expect(person).toBeInstanceOf(Person); expect(person.hobby).not.toHaveProperty('__type'); }).not.toThrow(); }); it('should transform class Person into class OtherPerson with different types in array (polymorphism)', () => { defaultMetadataStorage.clear(); expect(() => { abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [ { value: Sports, name: 'sports' }, { value: Relaxing, name: 'relax' }, { value: Programming, name: 'program' }, ], }, }) public hobbies: any[]; } const model: Person = new Person(); const sport = new Sports(); sport.name = 'Football'; const program = new Programming(); program.name = 'typescript coding'; program.specialAbility = 'testing'; model.name = 'John Doe'; model.hobbies = [sport, program]; const person: Person = instanceToInstance(model); expect(person).toBeInstanceOf(Person); expect(person.hobbies[0]).not.toHaveProperty('__type'); expect(person.hobbies[1]).not.toHaveProperty('__type'); }).not.toThrow(); }); it('should serialize json into model instance of class Person with different possibilities for type of one property AND uses default as fallback (polymorphism)', () => { defaultMetadataStorage.clear(); expect(() => { const json = { name: 'John Doe', hobby: { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, }; abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [], }, }) public hobby: any; } const model: Person = plainToInstance(Person, json); expect(model).toBeInstanceOf(Person); expect(model.hobby).toBeInstanceOf(Hobby); expect(model.hobby).not.toHaveProperty('__type'); expect(model.hobby).toHaveProperty('specialAbility', 'testing'); }).not.toThrow(); }); it('should serialize json into model instance of class Person with different types in array AND uses default as fallback (polymorphism)', () => { defaultMetadataStorage.clear(); expect(() => { const json = { name: 'John Doe', hobbies: [ { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, { __type: 'relax', name: 'sun' }, ], }; abstract class Hobby { public name: string; } class Sports extends Hobby { // Empty } class Relaxing extends Hobby { // Empty } class Programming extends Hobby { @Transform(({ value }) => value.toUpperCase()) specialAbility: string; } class Person { public name: string; @Type(() => Hobby, { discriminator: { property: '__type', subTypes: [], }, }) public hobbies: any[]; } const model: Person = plainToInstance(Person, json); expect(model).toBeInstanceOf(Person); expect(model.hobbies[0]).toBeInstanceOf(Hobby); expect(model.hobbies[1]).toBeInstanceOf(Hobby); expect(model.hobbies[0]).not.toHaveProperty('__type'); expect(model.hobbies[1]).not.toHaveProperty('__type'); expect(model.hobbies[1]).toHaveProperty('name', 'sun'); expect(model.hobbies[0]).toHaveProperty('specialAbility', 'testing'); }).not.toThrow(); }); it('should serialize a model into json', () => { expect(() => { instanceToPlain(model); }).not.toThrow(); }); }); ================================================ FILE: test/functional/default-values.spec.ts ================================================ import { Expose, plainToInstance, Transform } from '../../src'; describe('expose default values', () => { class User { @Expose({ name: 'AGE' }) @Transform(({ value }) => parseInt(value, 10)) age: number; @Expose({ name: 'AGE_WITH_DEFAULT' }) @Transform(({ value }) => parseInt(value, 10)) ageWithDefault?: number = 18; @Expose({ name: 'FIRST_NAME' }) firstName: string; @Expose({ name: 'FIRST_NAME_WITH_DEFAULT' }) firstNameWithDefault?: string = 'default first name'; @Transform(({ value }) => !!value) admin: boolean; @Transform(({ value }) => !!value) adminWithDefault?: boolean = false; lastName: string; lastNameWithDefault?: string = 'default last name'; } it('should set default value if nothing provided', () => { const fromPlainUser = {}; const transformedUser = plainToInstance(User, fromPlainUser, { exposeDefaultValues: true }); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ age: undefined, ageWithDefault: 18, firstName: undefined, firstNameWithDefault: 'default first name', adminWithDefault: false, lastNameWithDefault: 'default last name', }); }); it('should take exposed values and ignore defaults', () => { const fromPlainUser = {}; const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ age: NaN, ageWithDefault: NaN, firstName: undefined, firstNameWithDefault: undefined, adminWithDefault: false, lastNameWithDefault: 'default last name', }); }); }); ================================================ FILE: test/functional/es6-data-types.spec.ts ================================================ import 'reflect-metadata'; import { instanceToPlain, plainToInstance, Expose } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; import { Type } from '../../src/decorators'; describe('es6 data types', () => { it('using Map', () => { defaultMetadataStorage.clear(); class User { id: number; name: string; @Type(() => String) weapons: Map; } const plainUser = { id: 1, name: 'Max Pain', weapons: { firstWeapon: 'knife', secondWeapon: 'eagle', thirdWeapon: 'ak-47', }, }; const weapons = new Map(); weapons.set('firstWeapon', 'knife'); weapons.set('secondWeapon', 'eagle'); weapons.set('thirdWeapon', 'ak-47'); const user = new User(); user.id = 1; user.name = 'Max Pain'; user.weapons = weapons; const classedUser = plainToInstance(User, plainUser); expect(classedUser).toBeInstanceOf(User); expect(classedUser.id).toEqual(1); expect(classedUser.name).toEqual('Max Pain'); expect(classedUser.weapons).toBeInstanceOf(Map); expect(classedUser.weapons.size).toEqual(3); expect(classedUser.weapons.get('firstWeapon')).toEqual('knife'); expect(classedUser.weapons.get('secondWeapon')).toEqual('eagle'); expect(classedUser.weapons.get('thirdWeapon')).toEqual('ak-47'); const plainedUser = instanceToPlain(user); expect(plainedUser).not.toBeInstanceOf(User); expect(plainedUser).toEqual({ id: 1, name: 'Max Pain', weapons: { firstWeapon: 'knife', secondWeapon: 'eagle', thirdWeapon: 'ak-47', }, }); }); it('using Set', () => { defaultMetadataStorage.clear(); class User { id: number; name: string; @Type(() => Set) weapons: Set; } const plainUser = { id: 1, name: 'Max Pain', weapons: ['knife', 'eagle', 'ak-47'], }; const weapons = new Set(); weapons.add('knife'); weapons.add('eagle'); weapons.add('ak-47'); const user = new User(); user.id = 1; user.name = 'Max Pain'; user.weapons = weapons; const classedUser = plainToInstance(User, plainUser); expect(classedUser).toBeInstanceOf(User); expect(classedUser.id).toEqual(1); expect(classedUser.name).toEqual('Max Pain'); expect(classedUser.weapons).toBeInstanceOf(Set); expect(classedUser.weapons.size).toEqual(3); expect(classedUser.weapons.has('knife')).toBeTruthy(); expect(classedUser.weapons.has('eagle')).toBeTruthy(); expect(classedUser.weapons.has('ak-47')).toBeTruthy(); const plainedUser = instanceToPlain(user); expect(plainedUser).not.toBeInstanceOf(User); expect(plainedUser).toEqual({ id: 1, name: 'Max Pain', weapons: ['knife', 'eagle', 'ak-47'], }); }); it('using Map with objects', () => { defaultMetadataStorage.clear(); class Weapon { constructor(public model: string, public range: number) {} } class User { id: number; name: string; @Type(() => Weapon) weapons: Map; } const plainUser = { id: 1, name: 'Max Pain', weapons: { firstWeapon: { model: 'knife', range: 1, }, secondWeapon: { model: 'eagle', range: 200, }, thirdWeapon: { model: 'ak-47', range: 800, }, }, }; const weapons = new Map(); weapons.set('firstWeapon', new Weapon('knife', 1)); weapons.set('secondWeapon', new Weapon('eagle', 200)); weapons.set('thirdWeapon', new Weapon('ak-47', 800)); const user = new User(); user.id = 1; user.name = 'Max Pain'; user.weapons = weapons; const classedUser = plainToInstance(User, plainUser); expect(classedUser).toBeInstanceOf(User); expect(classedUser.id).toEqual(1); expect(classedUser.name).toEqual('Max Pain'); expect(classedUser.weapons).toBeInstanceOf(Map); expect(classedUser.weapons.size).toEqual(3); expect(classedUser.weapons.get('firstWeapon')).toBeInstanceOf(Weapon); expect(classedUser.weapons.get('firstWeapon')).toEqual({ model: 'knife', range: 1, }); expect(classedUser.weapons.get('secondWeapon')).toBeInstanceOf(Weapon); expect(classedUser.weapons.get('secondWeapon')).toEqual({ model: 'eagle', range: 200, }); expect(classedUser.weapons.get('thirdWeapon')).toBeInstanceOf(Weapon); expect(classedUser.weapons.get('thirdWeapon')).toEqual({ model: 'ak-47', range: 800, }); const plainedUser = instanceToPlain(user); expect(plainedUser).not.toBeInstanceOf(User); expect(plainedUser).toEqual({ id: 1, name: 'Max Pain', weapons: { firstWeapon: { model: 'knife', range: 1, }, secondWeapon: { model: 'eagle', range: 200, }, thirdWeapon: { model: 'ak-47', range: 800, }, }, }); }); it('using Set with objects', () => { defaultMetadataStorage.clear(); class Weapon { constructor(public model: string, public range: number) {} } class User { id: number; name: string; @Type(() => Weapon) weapons: Set; } const plainUser = { id: 1, name: 'Max Pain', weapons: [ { model: 'knife', range: 1 }, { model: 'eagle', range: 200 }, { model: 'ak-47', range: 800 }, ], }; const weapons = new Set(); weapons.add(new Weapon('knife', 1)); weapons.add(new Weapon('eagle', 200)); weapons.add(new Weapon('ak-47', 800)); const user = new User(); user.id = 1; user.name = 'Max Pain'; user.weapons = weapons; const classedUser = plainToInstance(User, plainUser); expect(classedUser).toBeInstanceOf(User); expect(classedUser.id).toEqual(1); expect(classedUser.name).toEqual('Max Pain'); expect(classedUser.weapons).toBeInstanceOf(Set); expect(classedUser.weapons.size).toEqual(3); const it = classedUser.weapons.values(); const first = it.next().value; const second = it.next().value; const third = it.next().value; expect(first).toBeInstanceOf(Weapon); expect(first).toEqual({ model: 'knife', range: 1 }); expect(second).toBeInstanceOf(Weapon); expect(second).toEqual({ model: 'eagle', range: 200 }); expect(third).toBeInstanceOf(Weapon); expect(third).toEqual({ model: 'ak-47', range: 800 }); const plainedUser = instanceToPlain(user); expect(plainedUser).not.toBeInstanceOf(User); expect(plainedUser).toEqual({ id: 1, name: 'Max Pain', weapons: [ { model: 'knife', range: 1 }, { model: 'eagle', range: 200 }, { model: 'ak-47', range: 800 }, ], }); }); it('using Map with objects with Expose', () => { defaultMetadataStorage.clear(); class Weapon { constructor(public model: string, public range: number) {} } class User { @Expose() id: number; @Expose() name: string; @Expose() @Type(() => Weapon) weapons: Map; } const plainUser = { id: 1, name: 'Max Pain', weapons: { firstWeapon: { model: 'knife', range: 1, }, secondWeapon: { model: 'eagle', range: 200, }, thirdWeapon: { model: 'ak-47', range: 800, }, }, }; const weapons = new Map(); weapons.set('firstWeapon', new Weapon('knife', 1)); weapons.set('secondWeapon', new Weapon('eagle', 200)); weapons.set('thirdWeapon', new Weapon('ak-47', 800)); const user = new User(); user.id = 1; user.name = 'Max Pain'; user.weapons = weapons; const plainedUser = instanceToPlain(user); expect(plainedUser).not.toBeInstanceOf(User); expect(plainedUser).toEqual({ id: 1, name: 'Max Pain', weapons: { firstWeapon: { model: 'knife', range: 1, }, secondWeapon: { model: 'eagle', range: 200, }, thirdWeapon: { model: 'ak-47', range: 800, }, }, }); function checkPlainToClassUser(classUser: User) { expect(classedUser).toBeInstanceOf(User); expect(classedUser.id).toEqual(1); expect(classedUser.name).toEqual('Max Pain'); expect(classedUser.weapons).toBeInstanceOf(Map); expect(classedUser.weapons.size).toEqual(3); expect(classedUser.weapons.get('firstWeapon')).toBeInstanceOf(Weapon); expect(classedUser.weapons.get('firstWeapon')).toEqual({ model: 'knife', range: 1, }); expect(classedUser.weapons.get('secondWeapon')).toBeInstanceOf(Weapon); expect(classedUser.weapons.get('secondWeapon')).toEqual({ model: 'eagle', range: 200, }); expect(classedUser.weapons.get('thirdWeapon')).toBeInstanceOf(Weapon); expect(classedUser.weapons.get('thirdWeapon')).toEqual({ model: 'ak-47', range: 800, }); } const classedUser = plainToInstance(User, plainUser, { excludeExtraneousValues: false }); checkPlainToClassUser(classedUser); const classedUser2 = plainToInstance(User, plainUser, { excludeExtraneousValues: true }); checkPlainToClassUser(classedUser2); }); }); ================================================ FILE: test/functional/ignore-decorators.spec.ts ================================================ import 'reflect-metadata'; import { instanceToPlain } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; import { Exclude, Expose } from '../../src/decorators'; describe('ignoring specific decorators', () => { it('when ignoreDecorators is set to true it should ignore all decorators', () => { defaultMetadataStorage.clear(); class User { id: number; @Expose({ name: 'lala' }) firstName: string; @Expose({ groups: ['user'] }) lastName: string; @Exclude() password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const plainedUser = instanceToPlain(user, { ignoreDecorators: true }); expect(plainedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); }); }); ================================================ FILE: test/functional/implicit-type-declarations.spec.ts ================================================ import 'reflect-metadata'; import { plainToInstance } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; import { Expose, Type } from '../../src/decorators'; describe('implicit type conversion', () => { it('should run only when enabled', () => { defaultMetadataStorage.clear(); class SimpleExample { @Expose() readonly implicitTypeNumber: number; @Expose() readonly implicitTypeString: string; } const result1: SimpleExample = plainToInstance( SimpleExample, { implicitTypeNumber: '100', implicitTypeString: 133123, }, { enableImplicitConversion: true } ); const result2: SimpleExample = plainToInstance( SimpleExample, { implicitTypeNumber: '100', implicitTypeString: 133123, }, { enableImplicitConversion: false } ); expect(result1).toEqual({ implicitTypeNumber: 100, implicitTypeString: '133123' }); expect(result2).toEqual({ implicitTypeNumber: '100', implicitTypeString: 133123 }); }); }); describe('implicit and explicity type declarations', () => { defaultMetadataStorage.clear(); class Example { @Expose() readonly implicitTypeViaOtherDecorator: Date; @Type() readonly implicitTypeViaEmptyTypeDecorator: number; @Type(() => String) readonly explicitType: string; } const result: Example = plainToInstance( Example, { implicitTypeViaOtherDecorator: '2018-12-24T12:00:00Z', implicitTypeViaEmptyTypeDecorator: '100', explicitType: 100, }, { enableImplicitConversion: true } ); it('should use implicitly defined design:type to convert value when no @Type decorator is used', () => { expect(result.implicitTypeViaOtherDecorator).toBeInstanceOf(Date); expect(result.implicitTypeViaOtherDecorator.getTime()).toEqual(new Date('2018-12-24T12:00:00Z').getTime()); }); it('should use implicitly defined design:type to convert value when empty @Type() decorator is used', () => { expect(typeof result.implicitTypeViaEmptyTypeDecorator).toBe('number'); expect(result.implicitTypeViaEmptyTypeDecorator).toEqual(100); }); it('should use explicitly defined type when @Type(() => Construtable) decorator is used', () => { expect(typeof result.explicitType).toBe('string'); expect(result.explicitType).toEqual('100'); }); }); describe('plainToInstance transforms built-in primitive types properly', () => { defaultMetadataStorage.clear(); class Example { @Type() date: Date; @Type() string: string; @Type() string2: string; @Type() number: number; @Type() number2: number; @Type() boolean: boolean; @Type() boolean2: boolean; } const result: Example = plainToInstance( Example, { date: '2018-12-24T12:00:00Z', string: '100', string2: 100, number: '100', number2: 100, boolean: 1, boolean2: 0, }, { enableImplicitConversion: true } ); it('should recognize and convert to Date', () => { expect(result.date).toBeInstanceOf(Date); expect(result.date.getTime()).toEqual(new Date('2018-12-24T12:00:00Z').getTime()); }); it('should recognize and convert to string', () => { expect(typeof result.string).toBe('string'); expect(typeof result.string2).toBe('string'); expect(result.string).toEqual('100'); expect(result.string2).toEqual('100'); }); it('should recognize and convert to number', () => { expect(typeof result.number).toBe('number'); expect(typeof result.number2).toBe('number'); expect(result.number).toEqual(100); expect(result.number2).toEqual(100); }); it('should recognize and convert to boolean', () => { expect(result.boolean).toBeTruthy(); expect(result.boolean2).toBeFalsy(); }); }); ================================================ FILE: test/functional/inheritence.spec.ts ================================================ import 'reflect-metadata'; import { plainToInstance, Transform, Type } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; describe('inheritence', () => { it('decorators should work inside a base class', () => { defaultMetadataStorage.clear(); class Contact { @Transform(({ value }) => value.toUpperCase()) name: string; @Type(() => Date) birthDate: Date; } class User extends Contact { @Type(() => Number) id: number; email: string; } class Student extends User { @Transform(({ value }) => value.toUpperCase()) university: string; } const plainStudent = { name: 'Johny Cage', university: 'mit', birthDate: new Date(1967, 2, 1).toDateString(), id: 100, email: 'johnny.cage@gmail.com', }; const classedStudent = plainToInstance(Student, plainStudent); expect(classedStudent.name).toEqual('JOHNY CAGE'); expect(classedStudent.university).toEqual('MIT'); expect(classedStudent.birthDate.getTime()).toEqual(new Date(1967, 2, 1).getTime()); expect(classedStudent.id).toEqual(plainStudent.id); expect(classedStudent.email).toEqual(plainStudent.email); }); }); ================================================ FILE: test/functional/prevent-array-bomb.spec.ts ================================================ import 'reflect-metadata'; import { plainToInstance } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; describe('Prevent array bomb when used with other packages', () => { it('should not convert specially crafted evil JS object to array', () => { defaultMetadataStorage.clear(); class TestClass { readonly categories!: string[]; } /** * We use the prototype of values to guess what is the type of the property. This behavior can be used * to pass a specially crafted array like object what would be transformed into an array. * * Because arrays are numerically indexed, specifying a big enough numerical property as key * would cause other libraries to iterate over each (undefined) element until the specified value is reached. * This can be used to cause denial-of-service attacks. * * An example of such scenario is the following: * * ```ts * class TestClass { * @IsArray() * @IsString({ each: true }) * readonly categories!: string[]; * } * ``` * * Using the above class definition with class-validator and receiving the following specially crafted payload without * the correct protection in place: * * `{ '9007199254740990': '9007199254740990', __proto__: [] };` * * would result in the creation of an array with length of 9007199254740991 (MAX_SAFE_INTEGER) looking like this: * * `[ <9007199254740989 empty elements>, 9007199254740990 ]` * * Iterating over this array would take significant time and cause the server to become unresponsive. */ const evilObject = { '100000000': '100000000', __proto__: [] }; const result = plainToInstance(TestClass, { categories: evilObject }); expect(Array.isArray(result.categories)).toBe(false); expect(result.categories).toEqual({ '100000000': '100000000' }); }); }); ================================================ FILE: test/functional/promise-field.spec.ts ================================================ import 'reflect-metadata'; import { defaultMetadataStorage } from '../../src/storage'; import { plainToInstance, Type, instanceToPlain } from '../../src'; describe('promise field', () => { it('should transform plan to class with promise field', async () => { defaultMetadataStorage.clear(); class PromiseClass { promise: Promise; } const plain = { promise: Promise.resolve('hi'), }; const instance = plainToInstance(PromiseClass, plain); expect(instance.promise).toBeInstanceOf(Promise); const value = await instance.promise; expect(value).toBe('hi'); }); it('should transform class with promise field to plain', async () => { class PromiseClass { promise: Promise; constructor(promise: Promise) { this.promise = promise; } } const instance = new PromiseClass(Promise.resolve('hi')); const plain = instanceToPlain(instance) as any; expect(plain).toHaveProperty('promise'); const value = await plain.promise; expect(value).toBe('hi'); }); it('should clone promise result', async () => { defaultMetadataStorage.clear(); class PromiseClass { promise: Promise; } const array = ['hi', 'my', 'name']; const plain = { promise: Promise.resolve(array), }; const instance = plainToInstance(PromiseClass, plain); const value = await instance.promise; expect(value).toEqual(array); // modify transformed array to prove it's not referencing original array value.push('is'); expect(value).not.toEqual(array); }); it('should support Type decorator', async () => { class PromiseClass { @Type(() => InnerClass) promise: Promise; } class InnerClass { position: string; constructor(position: string) { this.position = position; } } const plain = { promise: Promise.resolve(new InnerClass('developer')), }; const instance = plainToInstance(PromiseClass, plain); const value = await instance.promise; expect(value).toBeInstanceOf(InnerClass); expect(value.position).toBe('developer'); }); }); ================================================ FILE: test/functional/serialization-deserialization.spec.ts ================================================ import 'reflect-metadata'; import { deserialize, deserializeArray, serialize } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; import { Exclude } from '../../src/decorators'; describe('serialization and deserialization objects', () => { it('should perform serialization and deserialization properly', () => { defaultMetadataStorage.clear(); class User { firstName: string; lastName: string; @Exclude() password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const user1 = new User(); user1.firstName = 'Dima'; user1.lastName = 'Zotov'; user1.password = 'imnosuperman'; const user2 = new User(); user2.firstName = 'Bakhrom'; user2.lastName = 'Baubekov'; user2.password = 'imnosuperman'; const users = [user1, user2]; const plainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const plainUsers = [ { firstName: 'Dima', lastName: 'Zotov', password: 'imnobatman', }, { firstName: 'Bakhrom', lastName: 'Baubekov', password: 'imnosuperman', }, ]; const plainedUser = serialize(user); expect(plainedUser).toEqual( JSON.stringify({ firstName: 'Umed', lastName: 'Khudoiberdiev', }) ); const plainedUsers = serialize(users); expect(plainedUsers).toEqual( JSON.stringify([ { firstName: 'Dima', lastName: 'Zotov', }, { firstName: 'Bakhrom', lastName: 'Baubekov', }, ]) ); const classedUser = deserialize(User, JSON.stringify(plainUser)); expect(classedUser).toBeInstanceOf(User); expect(classedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classedUsers = deserializeArray(User, JSON.stringify(plainUsers)); expect(classedUsers[0]).toBeInstanceOf(User); expect(classedUsers[1]).toBeInstanceOf(User); const userLike1 = new User(); userLike1.firstName = 'Dima'; userLike1.lastName = 'Zotov'; const userLike2 = new User(); userLike2.firstName = 'Bakhrom'; userLike2.lastName = 'Baubekov'; expect(classedUsers).toEqual([userLike1, userLike2]); }); it('should successfully deserialize object with unknown nested properties ', () => { defaultMetadataStorage.clear(); class TestObject { prop: string; } const payload = { prop: 'Hi', extra: { anotherProp: "let's see how this works out!", }, }; const result = deserialize(TestObject, JSON.stringify(payload)); expect(result).toBeInstanceOf(TestObject); expect(result.prop).toEqual('Hi'); // TODO: We should strip, but it's a breaking change // (result).extra.should.be.undefined; }); it('should not overwrite non writable properties on deserialize', () => { class TestObject { get getterOnlyProp(): string { return 'I cannot write!'; } normalProp: string = 'Hello!'; } const payload = { getterOnlyProp: 'I CAN write!', normalProp: 'Goodbye!', }; const result = deserialize(TestObject, JSON.stringify(payload)); expect(result.getterOnlyProp).toEqual('I cannot write!'); expect(result.normalProp).toEqual('Goodbye!'); }); }); ================================================ FILE: test/functional/specify-maps.spec.ts ================================================ import 'reflect-metadata'; import { instanceToInstance, classToClassFromExist, instanceToPlain, classToPlainFromExist, plainToInstance, plainToClassFromExist, } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; import { Exclude, Expose, Type } from '../../src/decorators'; describe('specifying target maps', () => { it('should convert instance of the given object to plain javascript object and should expose all properties since its a default behaviour', () => { defaultMetadataStorage.clear(); class User { id: number; firstName: string; lastName: string; password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); const classToClassUser = instanceToInstance(user); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toBe(user); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); }); it('should exclude all objects marked with @Exclude() decorator', () => { defaultMetadataStorage.clear(); class User { id: number; firstName: string; lastName: string; @Exclude() password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); expect(plainUser.password).toBeUndefined(); const existUser = { id: 1, age: 27, password: 'yayayaya' }; const plainUser2 = classToPlainFromExist(user, existUser); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'yayayaya', }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassUser = instanceToInstance(user); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); }); it('should exclude all properties from object if whole class is marked with @Exclude() decorator', () => { defaultMetadataStorage.clear(); @Exclude() class User { id: number; firstName: string; lastName: string; password: string; } const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({}); expect(plainUser.firstName).toBeUndefined(); expect(plainUser.lastName).toBeUndefined(); expect(plainUser.password).toBeUndefined(); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({}); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, }); const classToClassUser = instanceToInstance(user); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({}); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, }); }); it('should exclude all properties from object if whole class is marked with @Exclude() decorator, but include properties marked with @Expose() decorator', () => { defaultMetadataStorage.clear(); @Exclude() class User { id: number; @Expose() firstName: string; @Expose() lastName: string; password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); expect(plainUser.password).toBeUndefined(); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassUser = instanceToInstance(user); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); }); it('should exclude all properties from object if its defined via transformation options, but include properties marked with @Expose() decorator', () => { defaultMetadataStorage.clear(); class User { id: number; @Expose() firstName: string; @Expose() lastName: string; password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user, { strategy: 'excludeAll' }); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); expect(plainUser.password).toBeUndefined(); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser, { strategy: 'excludeAll' }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser, { strategy: 'excludeAll' }); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser, { strategy: 'excludeAll' }); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassUser = instanceToInstance(user, { strategy: 'excludeAll' }); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser, { strategy: 'excludeAll' }); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', }); }); it('should expose all properties from object if its defined via transformation options, but exclude properties marked with @Exclude() decorator', () => { defaultMetadataStorage.clear(); class User { id: number; firstName: string; @Exclude() lastName: string; @Exclude() password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user, { strategy: 'exposeAll' }); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', }); expect(plainUser.lastName).toBeUndefined(); expect(plainUser.password).toBeUndefined(); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser, { strategy: 'exposeAll' }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser, { strategy: 'exposeAll' }); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: 'Umed', }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser, { strategy: 'exposeAll' }); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', }); const classToClassUser = instanceToInstance(user, { strategy: 'exposeAll' }); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({ firstName: 'Umed', }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser, { strategy: 'exposeAll' }); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', }); }); it('should convert values to specific types if they are set via @Type decorator', () => { defaultMetadataStorage.clear(); class User { id: number; @Type(type => String) firstName: string; @Type(type => String) lastName: string; @Type(type => Number) password: number; @Type(type => Boolean) isActive: boolean; @Type(type => Date) registrationDate: Date; @Type(type => String) lastVisitDate: string; } const date = new Date(); const user = new User(); user.firstName = 321 as any; user.lastName = 123 as any; user.password = '123' as any; user.isActive = '1' as any; user.registrationDate = date.toString() as any; user.lastVisitDate = date as any; const fromPlainUser = { firstName: 321, lastName: 123, password: '123', isActive: '1', registrationDate: date.toString(), lastVisitDate: date, }; const fromExistUser = new User(); fromExistUser.id = 1; const plainUser: any = instanceToPlain(user, { strategy: 'exposeAll' }); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), }); const existUser = { id: 1, age: 27 }; const plainUser2 = classToPlainFromExist(user, existUser, { strategy: 'exposeAll' }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), }); expect(plainUser2).toEqual(existUser); const transformedUser = plainToInstance(User, fromPlainUser, { strategy: 'exposeAll' }); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser).toEqual({ firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser, { strategy: 'exposeAll' }); expect(fromExistTransformedUser).toBeInstanceOf(User); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), }); const classToClassUser = instanceToInstance(user, { strategy: 'exposeAll' }); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).toEqual({ firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser, { strategy: 'exposeAll' }); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: '321', lastName: '123', password: 123, isActive: true, registrationDate: new Date(date.toString()), lastVisitDate: date.toString(), }); }); it('should transform nested objects too and make sure their decorators are used too', () => { defaultMetadataStorage.clear(); class Photo { id: number; name: string; @Exclude() filename: string; uploadDate: Date; } class User { firstName: string; lastName: string; @Exclude() password: string; photo: Photo; // type should be automatically guessed } const photo = new Photo(); photo.id = 1; photo.name = 'Me'; photo.filename = 'iam.jpg'; photo.uploadDate = new Date(); const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.photo = photo; const plainUser: any = instanceToPlain(user, { strategy: 'exposeAll' }); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser.photo).not.toBeInstanceOf(Photo); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', uploadDate: photo.uploadDate, }, }); expect(plainUser.password).toBeUndefined(); expect(plainUser.photo.filename).toBeUndefined(); expect(plainUser.photo.uploadDate).toEqual(photo.uploadDate); expect(plainUser.photo.uploadDate).not.toBe(photo.uploadDate); const existUser = { id: 1, age: 27, photo: { id: 2, description: 'photo' } }; const plainUser2: any = classToPlainFromExist(user, existUser, { strategy: 'exposeAll' }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2.photo).not.toBeInstanceOf(Photo); expect(plainUser2).toEqual({ id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', uploadDate: photo.uploadDate, description: 'photo', }, }); expect(plainUser2).toEqual(existUser); expect(plainUser2.password).toBeUndefined(); expect(plainUser2.photo.filename).toBeUndefined(); expect(plainUser2.photo.uploadDate).toEqual(photo.uploadDate); expect(plainUser2.photo.uploadDate).not.toBe(photo.uploadDate); }); it('should transform nested objects too and make sure given type is used instead of automatically guessed one', () => { defaultMetadataStorage.clear(); class Photo { id: number; name: string; @Exclude() filename: string; } class ExtendedPhoto implements Photo { id: number; @Exclude() name: string; filename: string; } class User { id: number; firstName: string; lastName: string; @Exclude() password: string; @Type(type => ExtendedPhoto) // force specific type photo: Photo; } const photo = new Photo(); photo.id = 1; photo.name = 'Me'; photo.filename = 'iam.jpg'; const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.photo = photo; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'iam.jpg', }, }); expect(plainUser.password).toBeUndefined(); expect(plainUser.photo.name).toBeUndefined(); }); it('should convert given plain object to class instance object', () => { defaultMetadataStorage.clear(); class Photo { id: number; name: string; @Exclude() filename: string; metadata: string; uploadDate: Date; } class User { id: number; firstName: string; lastName: string; @Exclude() password: string; @Type(type => Photo) photo: Photo; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.photo = new Photo(); user.photo.id = 1; user.photo.name = 'Me'; user.photo.filename = 'iam.jpg'; user.photo.uploadDate = new Date(); const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, name: 'Me', filename: 'iam.jpg', uploadDate: new Date(), }, }; const fromExistUser = new User(); fromExistUser.id = 1; const fromExistPhoto = new Photo(); fromExistPhoto.metadata = 'taken by Camera'; fromExistUser.photo = fromExistPhoto; const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); expect(transformedUser.photo).toBeInstanceOf(Photo); expect(transformedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', uploadDate: fromPlainUser.photo.uploadDate, }, }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser); expect(fromExistTransformedUser).toEqual(fromExistUser); expect(fromExistTransformedUser.photo).toEqual(fromExistPhoto); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', metadata: 'taken by Camera', uploadDate: fromPlainUser.photo.uploadDate, }, }); const classToClassUser = instanceToInstance(user); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser.photo).toBeInstanceOf(Photo); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).not.toEqual(user.photo); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', uploadDate: user.photo.uploadDate, }, }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser.photo).toBeInstanceOf(Photo); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).not.toEqual(user.photo); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, name: 'Me', metadata: 'taken by Camera', uploadDate: user.photo.uploadDate, }, }); }); it('should expose only properties that match given group', () => { defaultMetadataStorage.clear(); class Photo { id: number; @Expose({ groups: ['user', 'guest'], }) filename: string; @Expose({ groups: ['admin'], }) status: number; metadata: string; } class User { id: number; firstName: string; @Expose({ groups: ['user', 'guest'], }) lastName: string; @Expose({ groups: ['user'], }) password: string; @Expose({ groups: ['admin'], }) isActive: boolean; @Type(type => Photo) photo: Photo; @Expose({ groups: ['admin'], }) @Type(type => Photo) photos: Photo[]; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.isActive = false; user.photo = new Photo(); user.photo.id = 1; user.photo.filename = 'myphoto.jpg'; user.photo.status = 1; user.photos = [user.photo]; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', isActive: false, photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }; const fromExistUser = new User(); fromExistUser.id = 1; fromExistUser.photo = new Photo(); fromExistUser.photo.metadata = 'taken by Camera'; const plainUser1: any = instanceToPlain(user); expect(plainUser1).not.toBeInstanceOf(User); expect(plainUser1).toEqual({ firstName: 'Umed', photo: { id: 1, }, }); expect(plainUser1.lastName).toBeUndefined(); expect(plainUser1.password).toBeUndefined(); expect(plainUser1.isActive).toBeUndefined(); const plainUser2: any = instanceToPlain(user, { groups: ['user'] }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, filename: 'myphoto.jpg', }, }); expect(plainUser2.isActive).toBeUndefined(); const transformedUser2 = plainToInstance(User, fromPlainUser, { groups: ['user'] }); expect(transformedUser2).toBeInstanceOf(User); expect(transformedUser2.photo).toBeInstanceOf(Photo); expect(transformedUser2).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, filename: 'myphoto.jpg', }, }); const fromExistTransformedUser = plainToClassFromExist(fromExistUser, fromPlainUser, { groups: ['user'] }); expect(fromExistTransformedUser).toEqual(fromExistUser); expect(fromExistTransformedUser.photo).toEqual(fromExistUser.photo); expect(fromExistTransformedUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, metadata: 'taken by Camera', filename: 'myphoto.jpg', }, }); const classToClassUser = instanceToInstance(user, { groups: ['user'] }); expect(classToClassUser).toBeInstanceOf(User); expect(classToClassUser.photo).toBeInstanceOf(Photo); expect(classToClassUser).not.toEqual(user); expect(classToClassUser).not.toEqual(user.photo); expect(classToClassUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, filename: 'myphoto.jpg', }, }); const classToClassFromExistUser = classToClassFromExist(user, fromExistUser, { groups: ['user'] }); expect(classToClassFromExistUser).toBeInstanceOf(User); expect(classToClassFromExistUser.photo).toBeInstanceOf(Photo); expect(classToClassFromExistUser).not.toEqual(user); expect(classToClassFromExistUser).not.toEqual(user.photo); expect(classToClassFromExistUser).toEqual(fromExistUser); expect(classToClassFromExistUser).toEqual({ id: 1, firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, metadata: 'taken by Camera', filename: 'myphoto.jpg', }, }); const plainUser3: any = instanceToPlain(user, { groups: ['guest'] }); expect(plainUser3).not.toBeInstanceOf(User); expect(plainUser3).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', }, }); expect(plainUser3.password).toBeUndefined(); expect(plainUser3.isActive).toBeUndefined(); const transformedUser3 = plainToInstance(User, fromPlainUser, { groups: ['guest'] }); expect(transformedUser3).toBeInstanceOf(User); expect(transformedUser3.photo).toBeInstanceOf(Photo); expect(transformedUser3).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', }, }); const plainUser4: any = instanceToPlain(user, { groups: ['admin'] }); expect(plainUser4).not.toBeInstanceOf(User); expect(plainUser4).toEqual({ firstName: 'Umed', isActive: false, photo: { id: 1, status: 1, }, photos: [ { id: 1, status: 1, }, ], }); expect(plainUser4.lastName).toBeUndefined(); expect(plainUser4.password).toBeUndefined(); const transformedUser4 = plainToInstance(User, fromPlainUser, { groups: ['admin'] }); expect(transformedUser4).toBeInstanceOf(User); expect(transformedUser4.photo).toBeInstanceOf(Photo); expect(transformedUser4.photos[0]).toBeInstanceOf(Photo); expect(transformedUser4).toEqual({ firstName: 'Umed', isActive: false, photo: { id: 1, status: 1, }, photos: [ { id: 1, status: 1, }, ], }); const plainUser5: any = instanceToPlain(user, { groups: ['admin', 'user'] }); expect(plainUser5).not.toBeInstanceOf(User); expect(plainUser5).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', isActive: false, photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }); const transformedUser5 = plainToInstance(User, fromPlainUser, { groups: ['admin', 'user'] }); expect(transformedUser5).toBeInstanceOf(User); expect(transformedUser5).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', isActive: false, photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }); }); it('should expose only properties that match given version', () => { defaultMetadataStorage.clear(); class Photo { id: number; @Expose({ since: 1.5, until: 2, }) filename: string; @Expose({ since: 2, }) status: number; } class User { @Expose({ since: 1, until: 2, }) firstName: string; @Expose({ since: 0.5, }) lastName: string; @Exclude() password: string; @Type(type => Photo) photo: Photo; @Expose({ since: 3, }) @Type(type => Photo) photos: Photo[]; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.photo = new Photo(); user.photo.id = 1; user.photo.filename = 'myphoto.jpg'; user.photo.status = 1; user.photos = [user.photo]; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }; const plainUser1: any = instanceToPlain(user); expect(plainUser1).not.toBeInstanceOf(User); expect(plainUser1).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }); const transformedUser1 = plainToInstance(User, fromPlainUser); expect(transformedUser1).toBeInstanceOf(User); expect(transformedUser1.photo).toBeInstanceOf(Photo); expect(transformedUser1.photos[0]).toBeInstanceOf(Photo); expect(transformedUser1).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', status: 1, }, photos: [ { id: 1, filename: 'myphoto.jpg', status: 1, }, ], }); const plainUser2: any = instanceToPlain(user, { version: 0.3 }); expect(plainUser2).not.toBeInstanceOf(User); expect(plainUser2).toEqual({ photo: { id: 1, }, }); const transformedUser2 = plainToInstance(User, fromPlainUser, { version: 0.3 }); expect(transformedUser2).toBeInstanceOf(User); expect(transformedUser2.photo).toBeInstanceOf(Photo); expect(transformedUser2).toEqual({ photo: { id: 1, }, }); const plainUser3: any = instanceToPlain(user, { version: 0.5 }); expect(plainUser3).not.toBeInstanceOf(User); expect(plainUser3).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, }, }); const transformedUser3 = plainToInstance(User, fromPlainUser, { version: 0.5 }); expect(transformedUser3).toBeInstanceOf(User); expect(transformedUser3.photo).toBeInstanceOf(Photo); expect(transformedUser3).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, }, }); const plainUser4: any = instanceToPlain(user, { version: 1 }); expect(plainUser4).not.toBeInstanceOf(User); expect(plainUser4).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, }, }); const transformedUser4 = plainToInstance(User, fromPlainUser, { version: 1 }); expect(transformedUser4).toBeInstanceOf(User); expect(transformedUser4.photo).toBeInstanceOf(Photo); expect(transformedUser4).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, }, }); const plainUser5: any = instanceToPlain(user, { version: 1.5 }); expect(plainUser5).not.toBeInstanceOf(User); expect(plainUser5).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', }, }); const transformedUser5 = plainToInstance(User, fromPlainUser, { version: 1.5 }); expect(transformedUser5).toBeInstanceOf(User); expect(transformedUser5.photo).toBeInstanceOf(Photo); expect(transformedUser5).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', photo: { id: 1, filename: 'myphoto.jpg', }, }); const plainUser6: any = instanceToPlain(user, { version: 2 }); expect(plainUser6).not.toBeInstanceOf(User); expect(plainUser6).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, status: 1, }, }); const transformedUser6 = plainToInstance(User, fromPlainUser, { version: 2 }); expect(transformedUser6).toBeInstanceOf(User); expect(transformedUser6.photo).toBeInstanceOf(Photo); expect(transformedUser6).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, status: 1, }, }); const plainUser7: any = instanceToPlain(user, { version: 3 }); expect(plainUser7).not.toBeInstanceOf(User); expect(plainUser7).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, status: 1, }, photos: [ { id: 1, status: 1, }, ], }); const transformedUser7 = plainToInstance(User, fromPlainUser, { version: 3 }); expect(transformedUser7).toBeInstanceOf(User); expect(transformedUser7.photo).toBeInstanceOf(Photo); expect(transformedUser7.photos[0]).toBeInstanceOf(Photo); expect(transformedUser7).toEqual({ lastName: 'Khudoiberdiev', photo: { id: 1, status: 1, }, photos: [ { id: 1, status: 1, }, ], }); }); it('should expose method and accessors that have @Expose()', () => { defaultMetadataStorage.clear(); class User { firstName: string; lastName: string; @Exclude() password: string; @Expose() get name(): string { return this.firstName + ' ' + this.lastName; } @Expose() getName(): string { return this.firstName + ' ' + this.lastName; } } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', name: 'Umed Khudoiberdiev', getName: 'Umed Khudoiberdiev', }); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); const likeUser = new User(); likeUser.firstName = 'Umed'; likeUser.lastName = 'Khudoiberdiev'; expect(transformedUser).toEqual(likeUser); }); it('should expose with alternative name if its given', () => { defaultMetadataStorage.clear(); class User { @Expose({ name: 'myName' }) firstName: string; @Expose({ name: 'secondName' }) lastName: string; @Exclude() password: string; @Expose() get name(): string { return this.firstName + ' ' + this.lastName; } @Expose({ name: 'fullName' }) getName(): string { return this.firstName + ' ' + this.lastName; } } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const fromPlainUser = { myName: 'Umed', secondName: 'Khudoiberdiev', password: 'imnosuperman', }; const plainUser: any = instanceToPlain(user); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ myName: 'Umed', secondName: 'Khudoiberdiev', name: 'Umed Khudoiberdiev', fullName: 'Umed Khudoiberdiev', }); const transformedUser = plainToInstance(User, fromPlainUser); expect(transformedUser).toBeInstanceOf(User); const likeUser = new User(); likeUser.firstName = 'Umed'; likeUser.lastName = 'Khudoiberdiev'; expect(transformedUser).toEqual(likeUser); }); it('should exclude all prefixed properties if prefix is given', () => { defaultMetadataStorage.clear(); class Photo { id: number; $filename: string; status: number; } class User { $system: string; _firstName: string; _lastName: string; @Exclude() password: string; @Type(() => Photo) photo: Photo; @Expose() get name(): string { return this._firstName + ' ' + this._lastName; } } const user = new User(); user.$system = '@#$%^&*token(*&^%$#@!'; user._firstName = 'Umed'; user._lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; user.photo = new Photo(); user.photo.id = 1; user.photo.$filename = 'myphoto.jpg'; user.photo.status = 1; const fromPlainUser = { $system: '@#$%^&*token(*&^%$#@!', _firstName: 'Khudoiberdiev', _lastName: 'imnosuperman', password: 'imnosuperman', photo: { id: 1, $filename: 'myphoto.jpg', status: 1, }, }; const plainUser: any = instanceToPlain(user, { excludePrefixes: ['_', '$'] }); expect(plainUser).not.toBeInstanceOf(User); expect(plainUser).toEqual({ name: 'Umed Khudoiberdiev', photo: { id: 1, status: 1, }, }); const transformedUser = plainToInstance(User, fromPlainUser, { excludePrefixes: ['_', '$'] }); expect(transformedUser).toBeInstanceOf(User); const likeUser = new User(); likeUser.photo = new Photo(); likeUser.photo.id = 1; likeUser.photo.status = 1; expect(transformedUser).toEqual(likeUser); }); it('should be able to transform array too', () => { defaultMetadataStorage.clear(); class User { id: number; firstName: string; lastName: string; @Exclude() password: string; @Expose() get name(): string { return this.firstName + ' ' + this.lastName; } } const user1 = new User(); user1.firstName = 'Umed'; user1.lastName = 'Khudoiberdiev'; user1.password = 'imnosuperman'; const user2 = new User(); user2.firstName = 'Dima'; user2.lastName = 'Zotov'; user2.password = 'imnomesser'; const users = [user1, user2]; const plainUsers: any = instanceToPlain(users); expect(plainUsers).toEqual([ { firstName: 'Umed', lastName: 'Khudoiberdiev', name: 'Umed Khudoiberdiev', }, { firstName: 'Dima', lastName: 'Zotov', name: 'Dima Zotov', }, ]); const fromPlainUsers = [ { firstName: 'Umed', lastName: 'Khudoiberdiev', name: 'Umed Khudoiberdiev', }, { firstName: 'Dima', lastName: 'Zotov', name: 'Dima Zotov', }, ]; const existUsers = [ { id: 1, age: 27 }, { id: 2, age: 30 }, ]; const plainUser2 = classToPlainFromExist(users, existUsers); expect(plainUser2).toEqual([ { id: 1, age: 27, firstName: 'Umed', lastName: 'Khudoiberdiev', name: 'Umed Khudoiberdiev', }, { id: 2, age: 30, firstName: 'Dima', lastName: 'Zotov', name: 'Dima Zotov', }, ]); const transformedUser = plainToInstance(User, fromPlainUsers); expect(transformedUser[0]).toBeInstanceOf(User); expect(transformedUser[1]).toBeInstanceOf(User); const likeUser1 = new User(); likeUser1.firstName = 'Umed'; likeUser1.lastName = 'Khudoiberdiev'; const likeUser2 = new User(); likeUser2.firstName = 'Dima'; likeUser2.lastName = 'Zotov'; expect(transformedUser).toEqual([likeUser1, likeUser2]); const classToClassUsers = instanceToInstance(users); expect(classToClassUsers[0]).toBeInstanceOf(User); expect(classToClassUsers[1]).toBeInstanceOf(User); expect(classToClassUsers[0]).not.toEqual(user1); expect(classToClassUsers[1]).not.toEqual(user1); const classUserLike1 = new User(); classUserLike1.firstName = 'Umed'; classUserLike1.lastName = 'Khudoiberdiev'; const classUserLike2 = new User(); classUserLike2.firstName = 'Dima'; classUserLike2.lastName = 'Zotov'; expect(classToClassUsers).toEqual([classUserLike1, classUserLike2]); const fromExistUser1 = new User(); fromExistUser1.id = 1; const fromExistUser2 = new User(); fromExistUser2.id = 2; const fromExistUsers = [fromExistUser1, fromExistUser2]; const classToClassFromExistUser = classToClassFromExist(users, fromExistUsers); expect(classToClassFromExistUser[0]).toBeInstanceOf(User); expect(classToClassFromExistUser[1]).toBeInstanceOf(User); expect(classToClassFromExistUser[0]).not.toEqual(user1); expect(classToClassFromExistUser[1]).not.toEqual(user1); expect(classToClassFromExistUser).toEqual(fromExistUsers); const fromExistUserLike1 = new User(); fromExistUserLike1.id = 1; fromExistUserLike1.firstName = 'Umed'; fromExistUserLike1.lastName = 'Khudoiberdiev'; const fromExistUserLike2 = new User(); fromExistUserLike2.id = 2; fromExistUserLike2.firstName = 'Dima'; fromExistUserLike2.lastName = 'Zotov'; expect(classToClassFromExistUser).toEqual([fromExistUserLike1, fromExistUserLike2]); }); }); ================================================ FILE: test/functional/transformation-option.spec.ts ================================================ import 'reflect-metadata'; import { instanceToPlain, plainToInstance } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; import { Exclude, Expose } from '../../src/decorators'; describe('filtering by transformation option', () => { it('@Exclude with toPlainOnly set to true then it should be excluded only during instanceToPlain and classToPlainFromExist operations', () => { defaultMetadataStorage.clear(); class User { id: number; firstName: string; lastName: string; @Exclude({ toPlainOnly: true }) password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const plainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const plainedUser = instanceToPlain(user); expect(plainedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classedUser = plainToInstance(User, plainUser); expect(classedUser).toBeInstanceOf(User); expect(classedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); }); it('@Exclude with toClassOnly set to true then it should be excluded only during plainToInstance and plainToClassFromExist operations', () => { defaultMetadataStorage.clear(); class User { id: number; firstName: string; lastName: string; @Exclude({ toClassOnly: true }) password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const plainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const classedUser = plainToInstance(User, plainUser); expect(classedUser).toBeInstanceOf(User); expect(classedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const plainedUser = instanceToPlain(user); expect(plainedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); }); it('@Expose with toClassOnly set to true then it should be excluded only during instanceToPlain and classToPlainFromExist operations', () => { defaultMetadataStorage.clear(); @Exclude() class User { @Expose() firstName: string; @Expose() lastName: string; @Expose({ toClassOnly: true }) password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const plainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const plainedUser = instanceToPlain(user); expect(plainedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); const classedUser = plainToInstance(User, plainUser); expect(classedUser).toBeInstanceOf(User); expect(classedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); }); it('@Expose with toPlainOnly set to true then it should be excluded only during instanceToPlain and classToPlainFromExist operations', () => { defaultMetadataStorage.clear(); @Exclude() class User { @Expose() firstName: string; @Expose() lastName: string; @Expose({ toPlainOnly: true }) password: string; } const user = new User(); user.firstName = 'Umed'; user.lastName = 'Khudoiberdiev'; user.password = 'imnosuperman'; const plainUser = { firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }; const plainedUser = instanceToPlain(user); expect(plainedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', password: 'imnosuperman', }); const classedUser = plainToInstance(User, plainUser); expect(classedUser).toBeInstanceOf(User); expect(classedUser).toEqual({ firstName: 'Umed', lastName: 'Khudoiberdiev', }); }); it('should ignore undefined properties when exposeUnsetFields is set to false during class to plain', () => { defaultMetadataStorage.clear(); @Exclude() class User { @Expose() firstName: string; @Expose() lastName: string; } expect(instanceToPlain(new User(), { exposeUnsetFields: false })).toEqual({}); expect(instanceToPlain(new User(), { exposeUnsetFields: true })).toEqual({ firstName: undefined, lastName: undefined, }); const classedUser = plainToInstance(User, { exposeUnsetFields: false }); expect(classedUser).toBeInstanceOf(User); expect(classedUser).toEqual({ firstName: undefined, lastName: undefined, }); }); }); ================================================ FILE: test/functional/transformer-method.spec.ts ================================================ import 'reflect-metadata'; import { defaultMetadataStorage } from '../../src/storage'; import { Exclude, Expose, TransformInstanceToInstance, TransformInstanceToPlain, TransformPlainToInstance, } from '../../src/decorators'; describe('transformer methods decorator', () => { it('should expose non configuration properties and return User instance class', () => { defaultMetadataStorage.clear(); @Exclude() class User { id: number; @Expose() firstName: string; @Expose() lastName: string; password: string; } class UserController { @TransformInstanceToInstance() getUser(): User { const user = new User(); user.firstName = 'Snir'; user.lastName = 'Segal'; user.password = 'imnosuperman'; return user; } } const controller = new UserController(); const result = controller.getUser(); expect(result.password).toBeUndefined(); const plainUser = { firstName: 'Snir', lastName: 'Segal', }; expect(result).toEqual(plainUser); expect(result).toBeInstanceOf(User); }); it('should expose non configuration properties and return User instance class instead of plain object', () => { defaultMetadataStorage.clear(); @Exclude() class User { id: number; @Expose() firstName: string; @Expose() lastName: string; password: string; } class UserController { @TransformPlainToInstance(User) getUser(): User { const user: any = {}; user.firstName = 'Snir'; user.lastName = 'Segal'; user.password = 'imnosuperman'; return user; } } const controller = new UserController(); const result = controller.getUser(); expect(result.password).toBeUndefined(); const user = new User(); user.firstName = 'Snir'; user.lastName = 'Segal'; expect(result).toEqual(user); expect(result).toBeInstanceOf(User); }); it('should expose non configuration properties', () => { defaultMetadataStorage.clear(); @Exclude() class User { id: number; @Expose() firstName: string; @Expose() lastName: string; password: string; } class UserController { @TransformInstanceToPlain() getUser(): User { const user = new User(); user.firstName = 'Snir'; user.lastName = 'Segal'; user.password = 'imnosuperman'; return user; } } const controller = new UserController(); const result = controller.getUser(); expect(result.password).toBeUndefined(); const plainUser = { firstName: 'Snir', lastName: 'Segal', }; expect(result).toEqual(plainUser); }); it('should expose non configuration properties and properties with specific groups', () => { defaultMetadataStorage.clear(); @Exclude() class User { id: number; @Expose() firstName: string; @Expose() lastName: string; @Expose({ groups: ['user.permissions'] }) roles: string[]; password: string; } class UserController { @TransformInstanceToPlain({ groups: ['user.permissions'] }) getUserWithRoles(): User { const user = new User(); user.firstName = 'Snir'; user.lastName = 'Segal'; user.password = 'imnosuperman'; user.roles = ['USER', 'MANAGER']; return user; } } const controller = new UserController(); const result = controller.getUserWithRoles(); expect(result.password).toBeUndefined(); const plainUser = { firstName: 'Snir', lastName: 'Segal', roles: ['USER', 'MANAGER'], }; expect(result).toEqual(plainUser); }); it('should expose non configuration properties with specific version', () => { defaultMetadataStorage.clear(); @Exclude() class User { id: number; @Expose() firstName: string; @Expose() lastName: string; @Expose({ groups: ['user.permissions'] }) roles: string[]; @Expose({ since: 2 }) websiteUrl?: string; password: string; } class UserController { @TransformInstanceToPlain({ version: 1 }) getUserVersion1(): User { const user = new User(); user.firstName = 'Snir'; user.lastName = 'Segal'; user.password = 'imnosuperman'; user.roles = ['USER', 'MANAGER']; user.websiteUrl = 'http://www.github.com'; return user; } @TransformInstanceToPlain({ version: 2 }) getUserVersion2(): User { const user = new User(); user.firstName = 'Snir'; user.lastName = 'Segal'; user.password = 'imnosuperman'; user.roles = ['USER', 'MANAGER']; user.websiteUrl = 'http://www.github.com'; return user; } } const controller = new UserController(); const resultV2 = controller.getUserVersion2(); expect(resultV2.password).toBeUndefined(); expect(resultV2.roles).toBeUndefined(); const plainUserV2 = { firstName: 'Snir', lastName: 'Segal', websiteUrl: 'http://www.github.com', }; expect(resultV2).toEqual(plainUserV2); const resultV1 = controller.getUserVersion1(); expect(resultV1.password).toBeUndefined(); expect(resultV1.roles).toBeUndefined(); expect(resultV1.websiteUrl).toBeUndefined(); const plainUserV1 = { firstName: 'Snir', lastName: 'Segal', }; expect(resultV1).toEqual(plainUserV1); }); }); ================================================ FILE: test/functional/transformer-order.spec.ts ================================================ import 'reflect-metadata'; import { plainToInstance } from '../../src/index'; import { defaultMetadataStorage } from '../../src/storage'; import { Expose, Transform } from '../../src/decorators'; describe('applying several transformations', () => { beforeEach(() => defaultMetadataStorage.clear()); afterEach(() => defaultMetadataStorage.clear()); it('should keep the order of the applied decorators after several plainToInstance() calls', () => { class User { @Transform(() => 'Jonathan') @Transform(() => 'John') @Expose() name: string; } const firstUser = plainToInstance(User, { name: 'Joe' }); expect(firstUser.name).toEqual('John'); // Prior to this pull request [#355](https://github.com/typestack/class-transformer/pull/355) // the order of the transformations was reversed after every `plainToInstance()` call // So after consecutive calls `User#name` would be "John" - "Jonathan" - "John" - "Jonathan"... // This test ensures the last transformation is always the last one to be applied const secondUser = plainToInstance(User, { name: 'Joe' }); expect(secondUser.name).toEqual('John'); }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "moduleResolution": "node", "target": "es2018", "lib": ["es2018"], "outDir": "build/node", "rootDir": "./src", "strict": true, "sourceMap": true, "inlineSources": true, "removeComments": false, "esModuleInterop": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "forceConsistentCasingInFileNames": true }, "exclude": ["build", "node_modules", "sample", "**/*.spec.ts", "test/**"] } ================================================ FILE: tsconfig.prod.cjs.json ================================================ { "extends": "./tsconfig.prod.json", "compilerOptions": { "module": "CommonJS", "outDir": "build/cjs" }, } ================================================ FILE: tsconfig.prod.esm2015.json ================================================ { "extends": "./tsconfig.prod.json", "compilerOptions": { "module": "ES2015", "outDir": "build/esm2015", }, } ================================================ FILE: tsconfig.prod.esm5.json ================================================ { "extends": "./tsconfig.prod.json", "compilerOptions": { "module": "ES2015", "target": "ES5", "outDir": "build/esm5", }, } ================================================ FILE: tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "strict": false, "declaration": false, }, } ================================================ FILE: tsconfig.prod.types.json ================================================ { "extends": "./tsconfig.prod.json", "compilerOptions": { "declaration": true, "emitDeclarationOnly": true, "outDir": "build/types", }, } ================================================ FILE: tsconfig.spec.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "strict": false, "strictPropertyInitialization": false, "sourceMap": false, "removeComments": true, "noImplicitAny": false, }, "exclude": ["node_modules"] }