├── .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-lock.json ├── 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 /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: '@typescript-eslint/parser' 2 | plugins: 3 | - '@typescript-eslint' 4 | parserOptions: 5 | ecmaVersion: 2018 6 | sourceType: module 7 | project: 8 | - ./tsconfig.json 9 | - ./tsconfig.spec.json 10 | extends: 11 | - 'plugin:@typescript-eslint/recommended' 12 | - 'plugin:@typescript-eslint/recommended-requiring-type-checking' 13 | - 'plugin:jest/recommended' 14 | - 'prettier' 15 | rules: 16 | '@typescript-eslint/explicit-member-accessibility': off 17 | '@typescript-eslint/no-angle-bracket-type-assertion': off 18 | '@typescript-eslint/no-parameter-properties': off 19 | '@typescript-eslint/explicit-function-return-type': off 20 | '@typescript-eslint/member-delimiter-style': off 21 | '@typescript-eslint/no-inferrable-types': off 22 | '@typescript-eslint/no-explicit-any': off 23 | '@typescript-eslint/member-ordering': 'error' 24 | '@typescript-eslint/no-unused-vars': 25 | - 'error' 26 | - args: 'none' 27 | # TODO: Remove these and fixed issues once we merged all the current PRs. 28 | '@typescript-eslint/ban-types': off 29 | '@typescript-eslint/no-unsafe-return': off 30 | '@typescript-eslint/no-unsafe-assignment': off 31 | '@typescript-eslint/no-unsafe-call': off 32 | '@typescript-eslint/no-unsafe-member-access': off 33 | '@typescript-eslint/explicit-module-boundary-types': off -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs 2 | ​structure: 3 | readme: pages/01-getting-started.md 4 | summary: SUMMARY.md​ -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | timezone: Europe/Budapest 9 | open-pull-requests-limit: 0 10 | versioning-strategy: increase 11 | commit-message: 12 | prefix: build 13 | include: scope 14 | ignore: 15 | - dependency-name: "husky" 16 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | titleAndCommits: true 2 | allowMergeCommits: false 3 | scopes: 4 | - deps 5 | - deps-dev 6 | types: 7 | - feat 8 | - fix 9 | - docs 10 | - style 11 | - refactor 12 | - perf 13 | - test 14 | - build 15 | - ci 16 | - chore 17 | - revert 18 | - merge 19 | -------------------------------------------------------------------------------- /.github/workflows/auto-approve-dependabot-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: 3 | pull_request_target 4 | jobs: 5 | dependabot: 6 | runs-on: ubuntu-latest 7 | if: github.actor == 'dependabot[bot]' 8 | steps: 9 | - name: 'Auto approve PR by Dependabot' 10 | uses: hmarr/auto-approve-action@v2.0.0 11 | with: 12 | github-token: "${{ secrets.TYPESTACK_BOT_TOKEN }}" 13 | - name: 'Comment merge command' 14 | uses: actions/github-script@v3 15 | with: 16 | github-token: ${{secrets.TYPESTACK_BOT_TOKEN }} 17 | script: | 18 | await github.issues.createComment({ 19 | owner: context.repo.owner, 20 | repo: context.repo.repo, 21 | issue_number: context.issue.number, 22 | body: '@dependabot squash and merge' 23 | }) -------------------------------------------------------------------------------- /.github/workflows/continuous-deployment-workflow.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | publish: 7 | name: Publish to NPM 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions/setup-node@v1 12 | with: 13 | registry-url: https://registry.npmjs.org 14 | - run: npm ci --ignore-scripts 15 | - run: npm run prettier:check 16 | - run: npm run lint:check 17 | - run: npm run test:ci 18 | - run: npm run build:es2015 19 | - run: npm run build:esm5 20 | - run: npm run build:cjs 21 | - run: npm run build:umd 22 | - run: npm run build:types 23 | - run: cp LICENSE build/LICENSE 24 | - run: cp README.md build/README.md 25 | - run: jq 'del(.devDependencies) | del(.scripts)' package.json > build/package.json 26 | - run: npm publish ./build 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration-workflow.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | checks: 5 | name: Linters 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v1 9 | - uses: actions/setup-node@v1 10 | - run: npm ci --ignore-scripts 11 | - run: npm run prettier:check 12 | - run: npm run lint:check 13 | tests: 14 | name: Tests 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: ['10.x', '12.x', '14.x'] 19 | fail-fast: false 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: Setting up Node.js (v${{ matrix.node-version }}.x) 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci --ignore-scripts 27 | - run: npm run test:ci 28 | - run: npm install codecov -g 29 | if: ${{ matrix.node-version == '14.x' }} 30 | - run: codecov -f ./coverage/clover.xml -t ${{ secrets.CODECOV_TOKEN }} --commit=$GITHUB_SHA --branch=${GITHUB_REF##*/} 31 | if: ${{ matrix.node-version == '14.x' }} 32 | build: 33 | name: Build 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v1 37 | - uses: actions/setup-node@v1 38 | - run: npm ci --ignore-scripts 39 | - run: npm run build:es2015 40 | - run: npm run build:esm5 41 | - run: npm run build:cjs 42 | - run: npm run build:umd 43 | - run: npm run build:types 44 | -------------------------------------------------------------------------------- /.github/workflows/lock-closed-issues-workflow.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock inactive threads' 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | jobs: 6 | lock: 7 | name: Lock closed issues 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: dessant/lock-threads@v2 11 | with: 12 | github-token: ${{ github.token }} 13 | issue-lock-inactive-days: 30 14 | pr-lock-inactive-days: 30 15 | issue-lock-comment: > 16 | This issue has been automatically locked since there 17 | has not been any recent activity after it was closed. 18 | Please open a new issue for related bugs. 19 | pr-lock-comment: > 20 | This pull request has been automatically locked since there 21 | has not been any recent activity after it was closed. 22 | Please open a new issue for related bugs. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Log files 2 | logs 3 | *.log 4 | *.tmp 5 | *.tmp.* 6 | log.txt 7 | npm-debug.log* 8 | 9 | # Testing output 10 | lib-cov/** 11 | coverage/** 12 | 13 | # Environment files 14 | .env 15 | 16 | # Dependency directories 17 | node_modules 18 | 19 | # MacOS related files 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | ._* 24 | UserInterfaceState.xcuserstate 25 | 26 | # Windows related files 27 | Thumbs.db 28 | Desktop.ini 29 | $RECYCLE.BIN/ 30 | 31 | # IDE - Sublime 32 | *.sublime-project 33 | *.sublime-workspace 34 | 35 | # IDE - VSCode 36 | .vscode/** 37 | !.vscode/tasks.json 38 | !.vscode/launch.json 39 | 40 | # IDE - IntelliJ 41 | .idea 42 | 43 | # Compilation output folders 44 | dist/ 45 | build/ 46 | tmp/ 47 | out-tsc/ 48 | temp 49 | 50 | # Files for playing around locally 51 | playground.ts 52 | playground.js 53 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | tabWidth: 2 3 | useTabs: false 4 | semi: true 5 | singleQuote: true 6 | trailingComma: es5 7 | bracketSpacing: true 8 | arrowParens: avoid 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | _This changelog follows the [keep a changelog][keep-a-changelog]_ format to maintain a human readable changelog. 4 | 5 | ### [0.5.1][v0.5.1] [BREAKING CHANGE] - 2021-11-22 6 | 7 | #### Changed 8 | 9 | - re-added accidentally removed deprecated function names `classToPlain` and `plainToClass` 10 | 11 | ### [0.5.0][v0.5.0] [BREAKING CHANGE] - 2021-11-20 12 | 13 | > **NOTE:** This version fixes a security vulnerability allowing denial of service attacks with a specially crafted request payload. Please update as soon as possible. 14 | 15 | #### Breaking Changes 16 | 17 | See the breaking changes from `0.4.1` release. It was accidentally released as patch version. 18 | 19 | ### [0.4.1][v0.4.1] [BREAKING CHANGE] - 2021-11-20 20 | 21 | > **NOTE:** This version fixes a security vulnerability allowing denial of service attacks with a specially crafted request payload. Please update as soon as possible. 22 | 23 | #### Breaking Changes 24 | 25 | **Exported functions has been renamed** 26 | Some of the exported functions has been renamed to better reflect what they are doing. 27 | 28 | - `classToPlain` -> `instanceToPlain` 29 | - `plainToClass` -> `plainToInstance` 30 | - `classToClass` -> `instanceToInstance` 31 | 32 | #### Fixed 33 | 34 | - prevent unhandled error in `plaintToclass` when union-type member is undefined 35 | - fixed a scenario when a specially crafted JS object would be parsed to Array 36 | 37 | #### Changed 38 | 39 | - various dev-dependencies updated 40 | 41 | ### [0.4.0][v0.4.0] [BREAKING CHANGE] - 2021-02-14 42 | 43 | #### Breaking Changes 44 | 45 | See the breaking changes from `0.3.2` release. It was accidentally released as patch version. 46 | 47 | #### Added 48 | 49 | - add option to ignore unset properties 50 | - `group` information is exposed in the `@Transform` handler 51 | - transformation options are exposed in the `@Transform` handler 52 | 53 | #### Fixed 54 | 55 | - fixed TypeError when `discriminator.subTypes` is not defined 56 | 57 | #### Changed 58 | 59 | - various dev-dependencies has been updated 60 | 61 | ### [0.3.2][v0.3.2] [BREAKING CHANGE] - 2021-01-14 62 | 63 | #### Breaking Changes 64 | 65 | **Signature change for `@Transform` decorator** 66 | From this version the `@Transform` decorator receives the transformation parameters in a a wrapper object. You need to 67 | destructure the values you are interested in. 68 | 69 | Old way: 70 | 71 | ```ts 72 | @Transform((value, obj, type) => /* Do some stuff with value here. */) 73 | ``` 74 | 75 | New way with wrapper object: 76 | 77 | ```ts 78 | @Transform(({ value, key, obj, type }) => /* Do some stuff with value here. */) 79 | ``` 80 | 81 | #### Added 82 | 83 | - `exposeDefaultValues` option has been added, when enabled properties will use their default values when no value is present for the property 84 | - the name of the currently transformed parameter is exposed in the `@Transform` decorator 85 | 86 | #### Fixed 87 | 88 | - fixed an issue with transforming `Map` (#319) 89 | - fixed an issue with sourcemap generation (#472) 90 | 91 | #### Changed 92 | 93 | - various internal refactors 94 | - various changes to the project tooling 95 | - various dev-dependencies has been updated 96 | 97 | ### [0.3.1][v0.3.1] - 2020-07-29 98 | 99 | #### Added 100 | 101 | - table of content added to readme 102 | 103 | #### Changed 104 | 105 | - moved from Mocha to Jest 106 | - added Prettier for code formatting 107 | - added Eslint for linting 108 | - updated CI configuration 109 | - removed some unused dev dependencies 110 | - updated dependencies to latest version 111 | 112 | #### Fixed 113 | 114 | - circular dependency fixed 115 | - dev dependencies removed from package.json before publishing (no more security warnings) 116 | - transformer order is deterministic now (#231) 117 | - fix prototype pollution issue (#367) 118 | - various fixes in documentation 119 | 120 | ### [0.2.3][v0.2.3] [BREAKING CHANGE] 121 | 122 | #### Changed 123 | 124 | - `enableImplicitConversion` has been added and imlplicit value conversion is disabled by default. 125 | - reverted #234 - fix: write properties with defined default values on prototype which broke the `@Exclude` decorator. 126 | 127 | ### [0.2.2][v0.2.2] [BREAKING CHANGE] 128 | 129 | > **NOTE:** This version is deprecated. 130 | 131 | This version has introduced a breaking-change when this library is used with class-validator. See #257 for details. 132 | 133 | #### Added 134 | 135 | - implicity type conversion between values. 136 | 137 | ### [0.2.1][v0.2.1] 138 | 139 | > **NOTE:** This version is deprecated. 140 | 141 | #### Added 142 | 143 | - add option to strip unkown properties via using the `excludeExtraneousValues` option 144 | 145 | ### [0.2.0][v0.2.0] [BREAKING CHANGE] 146 | 147 | #### Added 148 | 149 | - add documentation for using `Set`s and `Map`s 150 | - add opotion to pass a discriminator function to convert values into different types based on custom conditions 151 | - added support for polymorphism based on a named type property 152 | 153 | #### Fixed 154 | 155 | - fix bug when transforming `null` values as primitives 156 | 157 | ### 0.1.10 158 | 159 | #### Fixed 160 | 161 | - improve MetadataStorage perf by changing from Arrays to ES6 Maps by @sheiidan 162 | - fixed getAncestor issue with unknown nested properties by @247GradLabs 163 | 164 | ### 0.1.9 165 | 166 | #### Fixed 167 | 168 | - objects with `null` prototype are converted properly now 169 | - objects with unknown non primitive properties are converted properly now 170 | - corrected a typo in the README.md 171 | - fixed the deserialize example in the README.md 172 | 173 | ### 0.1.4 174 | 175 | #### Added 176 | 177 | - added `TransformClassToPlain` and `TransformClassToClass` decorators 178 | 179 | ### 0.1.0 180 | 181 | #### Added 182 | 183 | - renamed library from `constructor-utils` to `class-transformer` 184 | - completely renamed most of names 185 | - renamed all main methods: `plainToConstructor` now is `plainToClass` and `constructorToPlain` is `classToPlain`, etc. 186 | - `plainToConstructorArray` method removed - now `plainToClass` handles it 187 | - `@Skip()` decorator renamed to `@Exclude()` 188 | - added `@Expose` decorator 189 | - added lot of new options: groups, versioning, custom names, etc. 190 | - methods and getters that should be exposed must be decorated with `@Expose` decorator 191 | - added `excludedPrefix` to class transform options that allows exclude properties that start with one of the given prefix 192 | 193 | ### 0.0.22 194 | 195 | #### Fixed 196 | 197 | - fixed array with primitive types being converted 198 | 199 | ### 0.0.18-0.0.21 200 | 201 | #### Fixed 202 | 203 | - fixed bugs when getters are not converted with es6 target 204 | 205 | ### 0.0.17 206 | 207 | #### Fixed 208 | 209 | - fixed issue #4 210 | - added type guessing during transformation from constructor to plain object 211 | - added sample with generics 212 | 213 | ### 0.0.16 214 | 215 | #### Changed 216 | 217 | - renamed `constructor-utils/constructor-utils` to `constructor-utils` package namespace 218 | 219 | ### 0.0.15 220 | 221 | #### Removed 222 | 223 | - removed code mappings from package 224 | 225 | ### 0.0.14 226 | 227 | #### Removed 228 | 229 | - removed `import "reflect-metadata"` from source code. Now reflect metadata should be included like any other shims. 230 | 231 | ### 0.0.13 232 | 233 | #### Changed 234 | 235 | - Library has changed its name from `serializer.ts` to `constructor-utils`. 236 | - Added `constructor-utils` namespace. 237 | 238 | [v0.5.1]: https://github.com/typestack/class-transformer/compare/v0.5.0...v0.5.1 239 | [v0.5.0]: https://github.com/typestack/class-transformer/compare/v0.4.1...v0.5.0 240 | [v0.4.1]: https://github.com/typestack/class-transformer/compare/v0.4.0...v0.4.1 241 | [v0.4.0]: https://github.com/typestack/class-transformer/compare/v0.3.2...v0.4.0 242 | [v0.3.2]: https://github.com/typestack/class-transformer/compare/v0.3.1...v0.3.2 243 | [v0.3.1]: https://github.com/typestack/class-transformer/compare/v0.2.3...v0.3.1 244 | [v0.2.3]: https://github.com/typestack/class-transformer/compare/v0.2.2...v0.2.3 245 | [v0.2.2]: https://github.com/typestack/class-transformer/compare/v0.2.1...v0.2.2 246 | [v0.2.1]: https://github.com/typestack/class-transformer/compare/v0.2.0...v0.2.1 247 | [v0.2.0]: https://github.com/typestack/class-transformer/compare/v0.1.10...v0.2.0 248 | [keep-a-changelog]: https://keepachangelog.com/en/1.0.0/ 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015-2020 TypeStack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # class-transformer 2 | 3 | ![Build Status](https://github.com/typestack/class-transformer/workflows/CI/badge.svg) 4 | [![codecov](https://codecov.io/gh/typestack/class-transformer/branch/develop/graph/badge.svg)](https://codecov.io/gh/typestack/class-transformer) 5 | [![npm version](https://badge.fury.io/js/class-transformer.svg)](https://badge.fury.io/js/class-transformer) 6 | 7 | Its ES6 and Typescript era. Nowadays you are working with classes and constructor objects more than ever. 8 | Class-transformer allows you to transform plain object to some instance of class and versa. 9 | Also it allows to serialize / deserialize object based on criteria. 10 | This tool is super useful on both frontend and backend. 11 | 12 | Example how to use with angular 2 in [plunker](http://plnkr.co/edit/Mja1ZYAjVySWASMHVB9R). 13 | Source code is available [here](https://github.com/pleerock/class-transformer-demo). 14 | 15 | ## Table of contents 16 | 17 | - [What is class-transformer](#what-is-class-transformer) 18 | - [Installation](#installation) 19 | - [Node.js](#nodejs) 20 | - [Browser](#browser) 21 | - [Methods](#methods) 22 | - [plainToInstance](#plaintoinstance) 23 | - [plainToClassFromExist](#plaintoclassfromexist) 24 | - [instanceToPlain](#instancetoplain) 25 | - [instanceToInstance](#instanceToInstance) 26 | - [serialize](#serialize) 27 | - [deserialize and deserializeArray](#deserialize-and-deserializearray) 28 | - [Enforcing type-safe instance](#enforcing-type-safe-instance) 29 | - [Working with nested objects](#working-with-nested-objects) 30 | - [Providing more than one type option](#providing-more-than-one-type-option) 31 | - [Exposing getters and method return values](#exposing-getters-and-method-return-values) 32 | - [Exposing properties with different names](#exposing-properties-with-different-names) 33 | - [Skipping specific properties](#skipping-specific-properties) 34 | - [Skipping depend of operation](#skipping-depend-of-operation) 35 | - [Skipping all properties of the class](#skipping-all-properties-of-the-class) 36 | - [Skipping private properties, or some prefixed properties](#skipping-private-properties-or-some-prefixed-properties) 37 | - [Using groups to control excluded properties](#using-groups-to-control-excluded-properties) 38 | - [Using versioning to control exposed and excluded properties](#using-versioning-to-control-exposed-and-excluded-properties) 39 | - [Сonverting date strings into Date objects](#сonverting-date-strings-into-date-objects) 40 | - [Working with arrays](#working-with-arrays) 41 | - [Additional data transformation](#additional-data-transformation) 42 | - [Basic usage](#basic-usage) 43 | - [Advanced usage](#advanced-usage) 44 | - [Other decorators](#other-decorators) 45 | - [Working with generics](#working-with-generics) 46 | - [Implicit type conversion](#implicit-type-conversion) 47 | - [How does it handle circular references?](#how-does-it-handle-circular-references) 48 | - [Example with Angular2](#example-with-angular2) 49 | - [Samples](#samples) 50 | - [Release notes](#release-notes) 51 | 52 | ## What is class-transformer[⬆](#table-of-contents) 53 | 54 | In JavaScript there are two types of objects: 55 | 56 | - plain (literal) objects 57 | - class (constructor) objects 58 | 59 | Plain objects are objects that are instances of `Object` class. 60 | Sometimes they are called **literal** objects, when created via `{}` notation. 61 | Class objects are instances of classes with own defined constructor, properties and methods. 62 | Usually you define them via `class` notation. 63 | 64 | So, what is the problem? 65 | 66 | Sometimes you want to transform plain javascript object to the ES6 **classes** you have. 67 | For example, if you are loading a json from your backend, some api or from a json file, 68 | and after you `JSON.parse` it you have a plain javascript object, not instance of class you have. 69 | 70 | For example you have a list of users in your `users.json` that you are loading: 71 | 72 | ```json 73 | [ 74 | { 75 | "id": 1, 76 | "firstName": "Johny", 77 | "lastName": "Cage", 78 | "age": 27 79 | }, 80 | { 81 | "id": 2, 82 | "firstName": "Ismoil", 83 | "lastName": "Somoni", 84 | "age": 50 85 | }, 86 | { 87 | "id": 3, 88 | "firstName": "Luke", 89 | "lastName": "Dacascos", 90 | "age": 12 91 | } 92 | ] 93 | ``` 94 | 95 | And you have a `User` class: 96 | 97 | ```typescript 98 | export class User { 99 | id: number; 100 | firstName: string; 101 | lastName: string; 102 | age: number; 103 | 104 | getName() { 105 | return this.firstName + ' ' + this.lastName; 106 | } 107 | 108 | isAdult() { 109 | return this.age > 36 && this.age < 60; 110 | } 111 | } 112 | ``` 113 | 114 | You are assuming that you are downloading users of type `User` from `users.json` file and may want to write 115 | following code: 116 | 117 | ```typescript 118 | fetch('users.json').then((users: User[]) => { 119 | // you can use users here, and type hinting also will be available to you, 120 | // but users are not actually instances of User class 121 | // this means that you can't use methods of User class 122 | }); 123 | ``` 124 | 125 | In this code you can use `users[0].id`, you can also use `users[0].firstName` and `users[0].lastName`. 126 | However you cannot use `users[0].getName()` or `users[0].isAdult()` because "users" actually is 127 | array of plain javascript objects, not instances of User object. 128 | You actually lied to compiler when you said that its `users: User[]`. 129 | 130 | So what to do? How to make a `users` array of instances of `User` objects instead of plain javascript objects? 131 | Solution is to create new instances of User object and manually copy all properties to new objects. 132 | But things may go wrong very fast once you have a more complex object hierarchy. 133 | 134 | Alternatives? Yes, you can use class-transformer. Purpose of this library is to help you to map your plain javascript 135 | objects to the instances of classes you have. 136 | 137 | This library also great for models exposed in your APIs, 138 | because it provides a great tooling to control what your models are exposing in your API. 139 | Here is an example how it will look like: 140 | 141 | ```typescript 142 | fetch('users.json').then((users: Object[]) => { 143 | const realUsers = plainToInstance(User, users); 144 | // now each user in realUsers is an instance of User class 145 | }); 146 | ``` 147 | 148 | Now you can use `users[0].getName()` and `users[0].isAdult()` methods. 149 | 150 | ## Installation[⬆](#table-of-contents) 151 | 152 | ### Node.js[⬆](#table-of-contents) 153 | 154 | 1. Install module: 155 | 156 | `npm install class-transformer --save` 157 | 158 | 2. `reflect-metadata` shim is required, install it too: 159 | 160 | `npm install reflect-metadata --save` 161 | 162 | and make sure to import it in a global place, like app.ts: 163 | 164 | ```typescript 165 | import 'reflect-metadata'; 166 | ``` 167 | 168 | 3. ES6 features are used, if you are using old version of node.js you may need to install es6-shim: 169 | 170 | `npm install es6-shim --save` 171 | 172 | and import it in a global place like app.ts: 173 | 174 | ```typescript 175 | import 'es6-shim'; 176 | ``` 177 | 178 | ### Browser[⬆](#table-of-contents) 179 | 180 | 1. Install module: 181 | 182 | `npm install class-transformer --save` 183 | 184 | 2. `reflect-metadata` shim is required, install it too: 185 | 186 | `npm install reflect-metadata --save` 187 | 188 | add ` 195 | 196 | 197 | 198 | ``` 199 | 200 | If you are using angular 2 you should already have this shim installed. 201 | 202 | 3. If you are using system.js you may want to add this into `map` and `package` config: 203 | 204 | ```json 205 | { 206 | "map": { 207 | "class-transformer": "node_modules/class-transformer" 208 | }, 209 | "packages": { 210 | "class-transformer": { "main": "index.js", "defaultExtension": "js" } 211 | } 212 | } 213 | ``` 214 | 215 | ## Methods[⬆](#table-of-contents) 216 | 217 | ### plainToInstance[⬆](#table-of-contents) 218 | 219 | This method transforms a plain javascript object to instance of specific class. 220 | 221 | ```typescript 222 | import { plainToInstance } from 'class-transformer'; 223 | 224 | let users = plainToInstance(User, userJson); // to convert user plain object a single user. also supports arrays 225 | ``` 226 | 227 | ### plainToClassFromExist[⬆](#table-of-contents) 228 | 229 | This method transforms a plain object into an instance using an already filled Object which is an instance of the target class. 230 | 231 | ```typescript 232 | const defaultUser = new User(); 233 | defaultUser.role = 'user'; 234 | 235 | let mixedUser = plainToClassFromExist(defaultUser, user); // mixed user should have the value role = user when no value is set otherwise. 236 | ``` 237 | 238 | ### instanceToPlain[⬆](#table-of-contents) 239 | 240 | This method transforms your class object back to plain javascript object, that can be `JSON.stringify` later. 241 | 242 | ```typescript 243 | import { instanceToPlain } from 'class-transformer'; 244 | let photo = instanceToPlain(photo); 245 | ``` 246 | 247 | ### instanceToInstance[⬆](#table-of-contents) 248 | 249 | This method transforms your class object into a new instance of the class object. 250 | This may be treated as deep clone of your objects. 251 | 252 | ```typescript 253 | import { instanceToInstance } from 'class-transformer'; 254 | let photo = instanceToInstance(photo); 255 | ``` 256 | 257 | You can also use an `ignoreDecorators` option in transformation options to ignore all decorators your classes are using. 258 | 259 | ### serialize[⬆](#table-of-contents) 260 | 261 | You can serialize your model right to json using `serialize` method: 262 | 263 | ```typescript 264 | import { serialize } from 'class-transformer'; 265 | let photo = serialize(photo); 266 | ``` 267 | 268 | `serialize` works with both arrays and non-arrays. 269 | 270 | ### deserialize and deserializeArray[⬆](#table-of-contents) 271 | 272 | You can deserialize your model from json using the `deserialize` method: 273 | 274 | ```typescript 275 | import { deserialize } from 'class-transformer'; 276 | let photo = deserialize(Photo, photo); 277 | ``` 278 | 279 | To make deserialization work with arrays, use the `deserializeArray` method: 280 | 281 | ```typescript 282 | import { deserializeArray } from 'class-transformer'; 283 | let photos = deserializeArray(Photo, photos); 284 | ``` 285 | 286 | ## Enforcing type-safe instance[⬆](#table-of-contents) 287 | 288 | The default behaviour of the `plainToInstance` method is to set _all_ properties from the plain object, 289 | even those which are not specified in the class. 290 | 291 | ```typescript 292 | import { plainToInstance } from 'class-transformer'; 293 | 294 | class User { 295 | id: number; 296 | firstName: string; 297 | lastName: string; 298 | } 299 | 300 | const fromPlainUser = { 301 | unkownProp: 'hello there', 302 | firstName: 'Umed', 303 | lastName: 'Khudoiberdiev', 304 | }; 305 | 306 | console.log(plainToInstance(User, fromPlainUser)); 307 | 308 | // User { 309 | // unkownProp: 'hello there', 310 | // firstName: 'Umed', 311 | // lastName: 'Khudoiberdiev', 312 | // } 313 | ``` 314 | 315 | If this behaviour does not suit your needs, you can use the `excludeExtraneousValues` option 316 | in the `plainToInstance` method while _exposing all your class properties_ as a requirement. 317 | 318 | ```typescript 319 | import { Expose, plainToInstance } from 'class-transformer'; 320 | 321 | class User { 322 | @Expose() id: number; 323 | @Expose() firstName: string; 324 | @Expose() lastName: string; 325 | } 326 | 327 | const fromPlainUser = { 328 | unkownProp: 'hello there', 329 | firstName: 'Umed', 330 | lastName: 'Khudoiberdiev', 331 | }; 332 | 333 | console.log(plainToInstance(User, fromPlainUser, { excludeExtraneousValues: true })); 334 | 335 | // User { 336 | // id: undefined, 337 | // firstName: 'Umed', 338 | // lastName: 'Khudoiberdiev' 339 | // } 340 | ``` 341 | 342 | ## Working with nested objects[⬆](#table-of-contents) 343 | 344 | When you are trying to transform objects that have nested objects, 345 | it's required to known what type of object you are trying to transform. 346 | Since Typescript does not have good reflection abilities yet, 347 | we should implicitly specify what type of object each property contain. 348 | This is done using `@Type` decorator. 349 | 350 | Lets say we have an album with photos. 351 | And we are trying to convert album plain object to class object: 352 | 353 | ```typescript 354 | import { Type, plainToInstance } from 'class-transformer'; 355 | 356 | export class Album { 357 | id: number; 358 | 359 | name: string; 360 | 361 | @Type(() => Photo) 362 | photos: Photo[]; 363 | } 364 | 365 | export class Photo { 366 | id: number; 367 | filename: string; 368 | } 369 | 370 | let album = plainToInstance(Album, albumJson); 371 | // now album is Album object with Photo objects inside 372 | ``` 373 | 374 | ### Providing more than one type option[⬆](#table-of-contents) 375 | 376 | In case the nested object can be of different types, you can provide an additional options object, 377 | that specifies a discriminator. The discriminator option must define a `property` that holds the subtype 378 | name for the object and the possible `subTypes` that the nested object can converted to. A sub type 379 | has a `value`, that holds the constructor of the Type and the `name`, that can match with the `property` 380 | of the discriminator. 381 | 382 | Lets say we have an album that has a top photo. But this photo can be of certain different types. 383 | And we are trying to convert album plain object to class object. The plain object input has to define 384 | the additional property `__type`. This property is removed during transformation by default: 385 | 386 | **JSON input**: 387 | 388 | ```json 389 | { 390 | "id": 1, 391 | "name": "foo", 392 | "topPhoto": { 393 | "id": 9, 394 | "filename": "cool_wale.jpg", 395 | "depth": 1245, 396 | "__type": "underwater" 397 | } 398 | } 399 | ``` 400 | 401 | ```typescript 402 | import { Type, plainToInstance } from 'class-transformer'; 403 | 404 | export abstract class Photo { 405 | id: number; 406 | filename: string; 407 | } 408 | 409 | export class Landscape extends Photo { 410 | panorama: boolean; 411 | } 412 | 413 | export class Portrait extends Photo { 414 | person: Person; 415 | } 416 | 417 | export class UnderWater extends Photo { 418 | depth: number; 419 | } 420 | 421 | export class Album { 422 | id: number; 423 | name: string; 424 | 425 | @Type(() => Photo, { 426 | discriminator: { 427 | property: '__type', 428 | subTypes: [ 429 | { value: Landscape, name: 'landscape' }, 430 | { value: Portrait, name: 'portrait' }, 431 | { value: UnderWater, name: 'underwater' }, 432 | ], 433 | }, 434 | }) 435 | topPhoto: Landscape | Portrait | UnderWater; 436 | } 437 | 438 | let album = plainToInstance(Album, albumJson); 439 | // now album is Album object with a UnderWater object without `__type` property. 440 | ``` 441 | 442 | Hint: The same applies for arrays with different sub types. Moreover you can specify `keepDiscriminatorProperty: true` 443 | in the options to keep the discriminator property also inside your resulting class. 444 | 445 | ## Exposing getters and method return values[⬆](#table-of-contents) 446 | 447 | You can expose what your getter or method return by setting an `@Expose()` decorator to those getters or methods: 448 | 449 | ```typescript 450 | import { Expose } from 'class-transformer'; 451 | 452 | export class User { 453 | id: number; 454 | firstName: string; 455 | lastName: string; 456 | password: string; 457 | 458 | @Expose() 459 | get name() { 460 | return this.firstName + ' ' + this.lastName; 461 | } 462 | 463 | @Expose() 464 | getFullName() { 465 | return this.firstName + ' ' + this.lastName; 466 | } 467 | } 468 | ``` 469 | 470 | ## Exposing properties with different names[⬆](#table-of-contents) 471 | 472 | If you want to expose some of the properties with a different name, 473 | you can do that by specifying a `name` option to `@Expose` decorator: 474 | 475 | ```typescript 476 | import { Expose } from 'class-transformer'; 477 | 478 | export class User { 479 | @Expose({ name: 'uid' }) 480 | id: number; 481 | 482 | firstName: string; 483 | 484 | lastName: string; 485 | 486 | @Expose({ name: 'secretKey' }) 487 | password: string; 488 | 489 | @Expose({ name: 'fullName' }) 490 | getFullName() { 491 | return this.firstName + ' ' + this.lastName; 492 | } 493 | } 494 | ``` 495 | 496 | ## Skipping specific properties[⬆](#table-of-contents) 497 | 498 | Sometimes you want to skip some properties during transformation. 499 | This can be done using `@Exclude` decorator: 500 | 501 | ```typescript 502 | import { Exclude } from 'class-transformer'; 503 | 504 | export class User { 505 | id: number; 506 | 507 | email: string; 508 | 509 | @Exclude() 510 | password: string; 511 | } 512 | ``` 513 | 514 | Now when you transform a User, the `password` property will be skipped and not be included in the transformed result. 515 | 516 | ## Skipping depend of operation[⬆](#table-of-contents) 517 | 518 | You can control on what operation you will exclude a property. Use `toClassOnly` or `toPlainOnly` options: 519 | 520 | ```typescript 521 | import { Exclude } from 'class-transformer'; 522 | 523 | export class User { 524 | id: number; 525 | 526 | email: string; 527 | 528 | @Exclude({ toPlainOnly: true }) 529 | password: string; 530 | } 531 | ``` 532 | 533 | Now `password` property will be excluded only during `instanceToPlain` operation. Vice versa, use the `toClassOnly` option. 534 | 535 | ## Skipping all properties of the class[⬆](#table-of-contents) 536 | 537 | You can skip all properties of the class, and expose only those are needed explicitly: 538 | 539 | ```typescript 540 | import { Exclude, Expose } from 'class-transformer'; 541 | 542 | @Exclude() 543 | export class User { 544 | @Expose() 545 | id: number; 546 | 547 | @Expose() 548 | email: string; 549 | 550 | password: string; 551 | } 552 | ``` 553 | 554 | Now `id` and `email` will be exposed, and password will be excluded during transformation. 555 | Alternatively, you can set exclusion strategy during transformation: 556 | 557 | ```typescript 558 | import { instanceToPlain } from 'class-transformer'; 559 | let photo = instanceToPlain(photo, { strategy: 'excludeAll' }); 560 | ``` 561 | 562 | In this case you don't need to `@Exclude()` a whole class. 563 | 564 | ## Skipping private properties, or some prefixed properties[⬆](#table-of-contents) 565 | 566 | If you name your private properties with a prefix, lets say with `_`, 567 | then you can exclude such properties from transformation too: 568 | 569 | ```typescript 570 | import { instanceToPlain } from 'class-transformer'; 571 | let photo = instanceToPlain(photo, { excludePrefixes: ['_'] }); 572 | ``` 573 | 574 | This will skip all properties that start with `_` prefix. 575 | You can pass any number of prefixes and all properties that begin with these prefixes will be ignored. 576 | For example: 577 | 578 | ```typescript 579 | import { Expose, instanceToPlain } from 'class-transformer'; 580 | 581 | export class User { 582 | id: number; 583 | private _firstName: string; 584 | private _lastName: string; 585 | _password: string; 586 | 587 | setName(firstName: string, lastName: string) { 588 | this._firstName = firstName; 589 | this._lastName = lastName; 590 | } 591 | 592 | @Expose() 593 | get name() { 594 | return this._firstName + ' ' + this._lastName; 595 | } 596 | } 597 | 598 | const user = new User(); 599 | user.id = 1; 600 | user.setName('Johny', 'Cage'); 601 | user._password = '123'; 602 | 603 | const plainUser = instanceToPlain(user, { excludePrefixes: ['_'] }); 604 | // here plainUser will be equal to 605 | // { id: 1, name: "Johny Cage" } 606 | ``` 607 | 608 | ## Using groups to control excluded properties[⬆](#table-of-contents) 609 | 610 | You can use groups to control what data will be exposed and what will not be: 611 | 612 | ```typescript 613 | import { Exclude, Expose, instanceToPlain } from 'class-transformer'; 614 | 615 | export class User { 616 | id: number; 617 | 618 | name: string; 619 | 620 | @Expose({ groups: ['user', 'admin'] }) // this means that this data will be exposed only to users and admins 621 | email: string; 622 | 623 | @Expose({ groups: ['user'] }) // this means that this data will be exposed only to users 624 | password: string; 625 | } 626 | 627 | let user1 = instanceToPlain(user, { groups: ['user'] }); // will contain id, name, email and password 628 | let user2 = instanceToPlain(user, { groups: ['admin'] }); // will contain id, name and email 629 | ``` 630 | 631 | ## Using versioning to control exposed and excluded properties[⬆](#table-of-contents) 632 | 633 | If you are building an API that has different versions, class-transformer has extremely useful tools for that. 634 | You can control which properties of your model should be exposed or excluded in what version. Example: 635 | 636 | ```typescript 637 | import { Exclude, Expose, instanceToPlain } from 'class-transformer'; 638 | 639 | export class User { 640 | id: number; 641 | 642 | name: string; 643 | 644 | @Expose({ since: 0.7, until: 1 }) // this means that this property will be exposed for version starting from 0.7 until 1 645 | email: string; 646 | 647 | @Expose({ since: 2.1 }) // this means that this property will be exposed for version starting from 2.1 648 | password: string; 649 | } 650 | 651 | let user1 = instanceToPlain(user, { version: 0.5 }); // will contain id and name 652 | let user2 = instanceToPlain(user, { version: 0.7 }); // will contain id, name and email 653 | let user3 = instanceToPlain(user, { version: 1 }); // will contain id and name 654 | let user4 = instanceToPlain(user, { version: 2 }); // will contain id and name 655 | let user5 = instanceToPlain(user, { version: 2.1 }); // will contain id, name and password 656 | ``` 657 | 658 | ## Сonverting date strings into Date objects[⬆](#table-of-contents) 659 | 660 | Sometimes you have a Date in your plain javascript object received in a string format. 661 | And you want to create a real javascript Date object from it. 662 | You can do it simply by passing a Date object to the `@Type` decorator: 663 | 664 | ```typescript 665 | import { Type } from 'class-transformer'; 666 | 667 | export class User { 668 | id: number; 669 | 670 | email: string; 671 | 672 | password: string; 673 | 674 | @Type(() => Date) 675 | registrationDate: Date; 676 | } 677 | ``` 678 | 679 | Same technique can be used with `Number`, `String`, `Boolean` 680 | primitive types when you want to convert your values into these types. 681 | 682 | ## Working with arrays[⬆](#table-of-contents) 683 | 684 | When you are using arrays you must provide a type of the object that array contains. 685 | This type, you specify in a `@Type()` decorator: 686 | 687 | ```typescript 688 | import { Type } from 'class-transformer'; 689 | 690 | export class Photo { 691 | id: number; 692 | 693 | name: string; 694 | 695 | @Type(() => Album) 696 | albums: Album[]; 697 | } 698 | ``` 699 | 700 | You can also use custom array types: 701 | 702 | ```typescript 703 | import { Type } from 'class-transformer'; 704 | 705 | export class AlbumCollection extends Array { 706 | // custom array functions ... 707 | } 708 | 709 | export class Photo { 710 | id: number; 711 | 712 | name: string; 713 | 714 | @Type(() => Album) 715 | albums: AlbumCollection; 716 | } 717 | ``` 718 | 719 | Library will handle proper transformation automatically. 720 | 721 | ES6 collections `Set` and `Map` also require the `@Type` decorator: 722 | 723 | ```typescript 724 | export class Skill { 725 | name: string; 726 | } 727 | 728 | export class Weapon { 729 | name: string; 730 | range: number; 731 | } 732 | 733 | export class Player { 734 | name: string; 735 | 736 | @Type(() => Skill) 737 | skills: Set; 738 | 739 | @Type(() => Weapon) 740 | weapons: Map; 741 | } 742 | ``` 743 | 744 | ## Additional data transformation[⬆](#table-of-contents) 745 | 746 | ### Basic usage[⬆](#table-of-contents) 747 | 748 | You can perform additional data transformation using `@Transform` decorator. 749 | For example, you want to make your `Date` object to be a `moment` object when you are 750 | transforming object from plain to class: 751 | 752 | ```typescript 753 | import { Transform } from 'class-transformer'; 754 | import * as moment from 'moment'; 755 | import { Moment } from 'moment'; 756 | 757 | export class Photo { 758 | id: number; 759 | 760 | @Type(() => Date) 761 | @Transform(({ value }) => moment(value), { toClassOnly: true }) 762 | date: Moment; 763 | } 764 | ``` 765 | 766 | Now when you call `plainToInstance` and send a plain representation of the Photo object, 767 | it will convert a date value in your photo object to moment date. 768 | `@Transform` decorator also supports groups and versioning. 769 | 770 | ### Advanced usage[⬆](#table-of-contents) 771 | 772 | The `@Transform` decorator is given more arguments to let you configure how you want the transformation to be done. 773 | 774 | ```ts 775 | @Transform(({ value, key, obj, type }) => value) 776 | ``` 777 | 778 | | Argument | Description | 779 | | --------- | ------------------------------------------------------- | 780 | | `value` | The property value before the transformation. | 781 | | `key` | The name of the transformed property. | 782 | | `obj` | The transformation source object. | 783 | | `type` | The transformation type. | 784 | | `options` | The options object passed to the transformation method. | 785 | 786 | ## Other decorators[⬆](#table-of-contents) 787 | 788 | | Signature | Example | Description | 789 | | ------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------- | 790 | | `@TransformClassToPlain` | `@TransformClassToPlain({ groups: ["user"] })` | Transform the method return with instanceToPlain and expose the properties on the class. | 791 | | `@TransformClassToClass` | `@TransformClassToClass({ groups: ["user"] })` | Transform the method return with instanceToInstance and expose the properties on the class. | 792 | | `@TransformPlainToClass` | `@TransformPlainToClass(User, { groups: ["user"] })` | Transform the method return with plainToInstance and expose the properties on the class. | 793 | 794 | The above decorators accept one optional argument: 795 | ClassTransformOptions - The transform options like groups, version, name 796 | 797 | An example: 798 | 799 | ```typescript 800 | @Exclude() 801 | class User { 802 | id: number; 803 | 804 | @Expose() 805 | firstName: string; 806 | 807 | @Expose() 808 | lastName: string; 809 | 810 | @Expose({ groups: ['user.email'] }) 811 | email: string; 812 | 813 | password: string; 814 | } 815 | 816 | class UserController { 817 | @TransformClassToPlain({ groups: ['user.email'] }) 818 | getUser() { 819 | const user = new User(); 820 | user.firstName = 'Snir'; 821 | user.lastName = 'Segal'; 822 | user.password = 'imnosuperman'; 823 | 824 | return user; 825 | } 826 | } 827 | 828 | const controller = new UserController(); 829 | const user = controller.getUser(); 830 | ``` 831 | 832 | the `user` variable will contain only firstName,lastName, email properties because they are 833 | the exposed variables. email property is also exposed because we metioned the group "user.email". 834 | 835 | ## Working with generics[⬆](#table-of-contents) 836 | 837 | Generics are not supported because TypeScript does not have good reflection abilities yet. 838 | Once TypeScript team provide us better runtime type reflection tools, generics will be implemented. 839 | There are some tweaks however you can use, that maybe can solve your problem. 840 | [Checkout this example.](https://github.com/pleerock/class-transformer/tree/master/sample/sample4-generics) 841 | 842 | ## Implicit type conversion[⬆](#table-of-contents) 843 | 844 | > **NOTE** If you use class-validator together with class-transformer you propably DON'T want to enable this function. 845 | 846 | Enables automatic conversion between built-in types based on type information provided by Typescript. Disabled by default. 847 | 848 | ```ts 849 | import { IsString } from 'class-validator'; 850 | 851 | class MyPayload { 852 | @IsString() 853 | prop: string; 854 | } 855 | 856 | const result1 = plainToInstance(MyPayload, { prop: 1234 }, { enableImplicitConversion: true }); 857 | const result2 = plainToInstance(MyPayload, { prop: 1234 }, { enableImplicitConversion: false }); 858 | 859 | /** 860 | * result1 will be `{ prop: "1234" }` - notice how the prop value has been converted to string. 861 | * result2 will be `{ prop: 1234 }` - default behaviour 862 | */ 863 | ``` 864 | 865 | ## How does it handle circular references?[⬆](#table-of-contents) 866 | 867 | Circular references are ignored. 868 | For example, if you are transforming class `User` that contains property `photos` with type of `Photo`, 869 | and `Photo` contains link `user` to its parent `User`, then `user` will be ignored during transformation. 870 | Circular references are not ignored only during `instanceToInstance` operation. 871 | 872 | ## Example with Angular2[⬆](#table-of-contents) 873 | 874 | Lets say you want to download users and want them automatically to be mapped to the instances of `User` class. 875 | 876 | ```typescript 877 | import { plainToInstance } from 'class-transformer'; 878 | 879 | this.http 880 | .get('users.json') 881 | .map(res => res.json()) 882 | .map(res => plainToInstance(User, res as Object[])) 883 | .subscribe(users => { 884 | // now "users" is type of User[] and each user has getName() and isAdult() methods available 885 | console.log(users); 886 | }); 887 | ``` 888 | 889 | You can also inject a class `ClassTransformer` as a service in `providers`, and use its methods. 890 | 891 | Example how to use with angular 2 in [plunker](http://plnkr.co/edit/Mja1ZYAjVySWASMHVB9R). 892 | Source code is [here](https://github.com/pleerock/class-transformer-demo). 893 | 894 | ## Samples[⬆](#table-of-contents) 895 | 896 | Take a look on samples in [./sample](https://github.com/pleerock/class-transformer/tree/master/sample) for more examples of 897 | usages. 898 | 899 | ## Release notes[⬆](#table-of-contents) 900 | 901 | See information about breaking changes and release notes [here](https://github.com/typestack/class-transformer/blob/master/CHANGELOG.md). 902 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 70..100 3 | round: down 4 | precision: 2 5 | status: 6 | project: 7 | default: 8 | threshold: 0% 9 | paths: 10 | - src/**/*.ts 11 | comment: off 12 | ignore: 13 | - testing/**/*.ts 14 | - src/**/*.interface.ts 15 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | - [Getting Started](pages/01-getting-started.md) 4 | - [Basic usage](pages/02-basis-usage.md) 5 | -------------------------------------------------------------------------------- /docs/pages/01-getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | The `class-transformer` package is a zero-dependency utility library helping you to quickly transform class instances to plain objects and vice-versa. 4 | It works well with the [`class-validator`][class-validator] library. The main features include: 5 | 6 | - conditionally transforming object properties 7 | - excluding specific properties from the transformed object 8 | - exposing properties under a different name on the transformed object 9 | - supports both NodeJS and browsers 10 | - fully three-shakable 11 | - zero external dependencies 12 | 13 | ## Installation 14 | 15 | To start using class-transformer install the required packages via NPM: 16 | 17 | ```bash 18 | npm install class-transformer reflect-metadata 19 | ``` 20 | 21 | Import the `reflect-metadata` package at the **first line** of your application: 22 | 23 | ```ts 24 | import 'reflect-metadata'; 25 | 26 | // Your other imports and initialization code 27 | // comes here after you imported the reflect-metadata package! 28 | ``` 29 | 30 | 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: 31 | 32 | ```json 33 | "emitDecoratorMetadata": true, 34 | "experimentalDecorators": true, 35 | ``` 36 | 37 | Now you are ready to use class-transformer with Typescript! 38 | 39 | ## Basic Usage 40 | 41 | The most basic usage is to transform a class to a plain object: 42 | 43 | ```ts 44 | import { Expose, Exclude, classToInstance } from 'class-transformer'; 45 | 46 | class User { 47 | /** 48 | * When transformed to plain the `_id` property will be remapped to `id` 49 | * in the plain object. 50 | */ 51 | @Expose({ name: 'id' }) 52 | private _id: string; 53 | 54 | /** 55 | * Expose the `name` property as it is in the plain object. 56 | */ 57 | @Expose() 58 | public name: string; 59 | 60 | /** 61 | * Exclude the `passwordHash` so it won't be included in the plain object. 62 | */ 63 | @Exclude() 64 | public passwordHash: string; 65 | } 66 | 67 | const user = getUserMagically(); 68 | // contains: User { _id: '42', name: 'John Snow', passwordHash: '2f55ce082...' } 69 | 70 | const plain = classToInstance(user); 71 | // contains { id: '42', name: 'John Snow' } 72 | ``` 73 | 74 | [class-validator]: https://github.com/typestack/class-validator/ 75 | -------------------------------------------------------------------------------- /docs/pages/02-basic-usage.md: -------------------------------------------------------------------------------- 1 | # Basic usage 2 | 3 | There are two main exported functions what can be used for transformations: 4 | 5 | - `plainToInstance` - transforms a plain object to an instance of the specified class constructor 6 | - `instanceToPlain` - transforms a _known_ class instance to a plain object 7 | 8 | Both function transforms the source object to the target via applying the metadata registered by the decorators on 9 | the class definition. The four main decorators are: 10 | 11 | - `@Expose` specifies how expose the given property on the plain object 12 | - `@Exclude` marks the property as skipped, so it won't show up in the transformation 13 | - `@Transform` allows specifying a custom transformation on the property via a custom handler 14 | - `@Type` decorator explicitly sets the type of the property, during the transformation `class-transformer` will attempt 15 | to create an instance of the specified type 16 | 17 | You must always decorate all your properties with an `@Expose` or `@Exclude` decorator. 18 | 19 | > **NOTE:** It's important to remember `class-transformer` will call the target type with am empty constructor, so if 20 | > you are using a type what requires special setup, you need to use a `@Transform` decorator and create the instance yourself. 21 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverageFrom: ['src/**/*.ts', '!src/**/index.ts', '!src/**/*.interface.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: 'tsconfig.spec.json', 8 | }, 9 | }, 10 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "class-transformer", 3 | "version": "0.5.1", 4 | "description": "Proper decorator-based transformation / serialization / deserialization of plain javascript objects to class constructors", 5 | "author": "TypeStack contributors", 6 | "license": "MIT", 7 | "readmeFilename": "README.md", 8 | "sideEffects": false, 9 | "main": "./cjs/index.js", 10 | "module": "./esm5/index.js", 11 | "es2015": "./esm2015/index.js", 12 | "typings": "./types/index.d.ts", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/typestack/class-transformer.git" 16 | }, 17 | "tags": [ 18 | "serialization", 19 | "deserialization", 20 | "serializer", 21 | "typescript", 22 | "object-to-class", 23 | "typescript-serializer" 24 | ], 25 | "scripts": { 26 | "build": "npm run build:cjs", 27 | "build:clean": "rimraf build", 28 | "build:es2015": "tsc --project tsconfig.prod.esm2015.json", 29 | "build:esm5": "tsc --project tsconfig.prod.esm5.json", 30 | "build:cjs": "tsc --project tsconfig.prod.cjs.json", 31 | "build:umd": "rollup --config rollup.config.js", 32 | "build:types": "tsc --project tsconfig.prod.types.json", 33 | "prettier:fix": "prettier --write \"**/*.{ts,md}\"", 34 | "prettier:check": "prettier --check \"**/*.{ts,md}\"", 35 | "lint:fix": "eslint --max-warnings 0 --fix --ext .ts src/", 36 | "lint:check": "eslint --max-warnings 0 --ext .ts src/", 37 | "test": "jest --coverage --verbose", 38 | "test:watch": "jest --watch", 39 | "test:ci": "jest --runInBand --no-cache --coverage --verbose" 40 | }, 41 | "husky": { 42 | "hooks": { 43 | "pre-commit": "lint-staged" 44 | } 45 | }, 46 | "lint-staged": { 47 | "*.md": [ 48 | "npm run prettier:fix" 49 | ], 50 | "*.ts": [ 51 | "npm run prettier:fix" 52 | ] 53 | }, 54 | "devDependencies": { 55 | "@rollup/plugin-commonjs": "^25.0.5", 56 | "@rollup/plugin-node-resolve": "^15.2.2", 57 | "@types/jest": "^27.5.0", 58 | "@types/node": "^20.8.5", 59 | "@typescript-eslint/eslint-plugin": "^4.33.0", 60 | "@typescript-eslint/parser": "^4.33.0", 61 | "eslint": "^7.32.0", 62 | "eslint-config-prettier": "^9.0.0", 63 | "eslint-plugin-jest": "^26.8.7", 64 | "husky": "^4.3.8", 65 | "jest": "^26.6.3", 66 | "lint-staged": "^14.0.1", 67 | "prettier": "^2.8.8", 68 | "reflect-metadata": "0.1.13", 69 | "rimraf": "5.0.5", 70 | "rollup": "^2.79.1", 71 | "rollup-plugin-terser": "^7.0.2", 72 | "ts-jest": "^26.5.6", 73 | "ts-node": "^10.9.1", 74 | "typescript": "^4.9.5" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | 5 | export default { 6 | input: 'build/esm5/index.js', 7 | output: [ 8 | { 9 | name: 'ClassTransformer', 10 | format: 'umd', 11 | file: 'build/bundles/class-transformer.umd.js', 12 | sourcemap: true, 13 | }, 14 | { 15 | name: 'ClassTransformer', 16 | format: 'umd', 17 | file: 'build/bundles/class-transformer.umd.min.js', 18 | sourcemap: true, 19 | plugins: [terser()], 20 | }, 21 | ], 22 | plugins: [commonjs(), nodeResolve()], 23 | }; -------------------------------------------------------------------------------- /sample/sample1-simple-usage/Album.ts: -------------------------------------------------------------------------------- 1 | import { Type, Exclude } from '../../src/decorators'; 2 | import { Photo } from './Photo'; 3 | 4 | export class Album { 5 | id: string; 6 | 7 | @Exclude() 8 | name: string; 9 | 10 | @Type(() => Photo) 11 | photos: Photo[]; 12 | } 13 | -------------------------------------------------------------------------------- /sample/sample1-simple-usage/Photo.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '../../src/decorators'; 2 | import { Album } from './Album'; 3 | import { User } from './User'; 4 | 5 | export class Photo { 6 | id: string; 7 | 8 | filename: string; 9 | 10 | description: string; 11 | 12 | tags: string[]; 13 | 14 | @Type(() => User) 15 | author: User; 16 | 17 | @Type(() => Album) 18 | albums: Album[]; 19 | 20 | get name() { 21 | return this.id + '_' + this.filename; 22 | } 23 | 24 | getAlbums() { 25 | console.log('this is not serialized/deserialized'); 26 | return this.albums; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sample/sample1-simple-usage/User.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '../../src/decorators'; 2 | 3 | export class User { 4 | @Type(() => Number) 5 | id: number; 6 | 7 | firstName: string; 8 | 9 | lastName: string; 10 | 11 | @Type(() => Date) 12 | registrationDate: Date; 13 | } 14 | -------------------------------------------------------------------------------- /sample/sample1-simple-usage/app.ts: -------------------------------------------------------------------------------- 1 | import 'es6-shim'; 2 | import 'reflect-metadata'; 3 | import { plainToClass, classToPlain } from '../../src/index'; 4 | import { Photo } from './Photo'; 5 | 6 | // check deserialization 7 | 8 | let photoJson = { 9 | id: '1', 10 | filename: 'myphoto.jpg', 11 | description: 'about my photo', 12 | tags: ['me', 'iam'], 13 | author: { 14 | id: '2', 15 | firstName: 'Johny', 16 | lastName: 'Cage', 17 | }, 18 | albums: [ 19 | { 20 | id: '1', 21 | name: 'My life', 22 | }, 23 | { 24 | id: '2', 25 | name: 'My young years', 26 | }, 27 | ], 28 | }; 29 | 30 | let photo = plainToClass(Photo, photoJson); 31 | console.log('deserialized object: ', photo); 32 | 33 | // now check serialization 34 | 35 | let newPhotoJson = classToPlain(photo); 36 | console.log('serialized object: ', newPhotoJson); 37 | 38 | // try to deserialize an array 39 | console.log('-------------------------------'); 40 | 41 | let photosJson = [ 42 | { 43 | id: '1', 44 | filename: 'myphoto.jpg', 45 | description: 'about my photo', 46 | author: { 47 | id: '2', 48 | firstName: 'Johny', 49 | lastName: 'Cage', 50 | registrationDate: '1995-12-17T03:24:00', 51 | }, 52 | albums: [ 53 | { 54 | id: '1', 55 | name: 'My life', 56 | }, 57 | { 58 | id: '2', 59 | name: 'My young years', 60 | }, 61 | ], 62 | }, 63 | { 64 | id: '2', 65 | filename: 'hisphoto.jpg', 66 | description: 'about his photo', 67 | author: { 68 | id: '2', 69 | firstName: 'Johny', 70 | lastName: 'Cage', 71 | }, 72 | albums: [ 73 | { 74 | id: '1', 75 | name: 'My life', 76 | }, 77 | { 78 | id: '2', 79 | name: 'My young years', 80 | }, 81 | ], 82 | }, 83 | ]; 84 | 85 | let photos = plainToClass(Photo, photosJson); 86 | console.log('deserialized array: ', photos); 87 | 88 | // now check array serialization 89 | 90 | let newPhotosJson = classToPlain(photos); 91 | console.log('serialized array: ', newPhotosJson); 92 | -------------------------------------------------------------------------------- /sample/sample2-iheritance/Album.ts: -------------------------------------------------------------------------------- 1 | import { Type, Exclude } from '../../src/decorators'; 2 | import { Photo } from './Photo'; 3 | import { Authorable } from './Authorable'; 4 | 5 | export class Album extends Authorable { 6 | id: string; 7 | 8 | @Exclude() 9 | name: string; 10 | 11 | @Type(() => Photo) 12 | photos: Photo[]; 13 | } 14 | -------------------------------------------------------------------------------- /sample/sample2-iheritance/Authorable.ts: -------------------------------------------------------------------------------- 1 | import { Type, Exclude } from '../../src/decorators'; 2 | import { User } from './User'; 3 | 4 | export class Authorable { 5 | authorName: string; 6 | 7 | @Exclude() 8 | authorEmail: string; 9 | 10 | @Type(() => User) 11 | author: User; 12 | } 13 | -------------------------------------------------------------------------------- /sample/sample2-iheritance/Photo.ts: -------------------------------------------------------------------------------- 1 | import { Type, Exclude } from '../../src/decorators'; 2 | import { Album } from './Album'; 3 | import { Authorable } from './Authorable'; 4 | 5 | export class Photo extends Authorable { 6 | id: string; 7 | 8 | filename: string; 9 | 10 | description: string; 11 | 12 | @Exclude() // this will ignore skipping inherited from Authorable class 13 | authorEmail: string; 14 | 15 | @Type(() => Album) 16 | albums: Album[]; 17 | } 18 | -------------------------------------------------------------------------------- /sample/sample2-iheritance/User.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '../../src/decorators'; 2 | 3 | export class User { 4 | @Type(() => Number) 5 | id: number; 6 | 7 | firstName: string; 8 | 9 | lastName: string; 10 | 11 | @Type(() => Date) 12 | registrationDate: Date; 13 | } 14 | -------------------------------------------------------------------------------- /sample/sample2-iheritance/app.ts: -------------------------------------------------------------------------------- 1 | import 'es6-shim'; 2 | import 'reflect-metadata'; 3 | import { classToPlain, plainToClass } from '../../src/index'; 4 | import { Photo } from './Photo'; 5 | 6 | let photoJson = { 7 | id: '1', 8 | filename: 'myphoto.jpg', 9 | description: 'about my photo', 10 | authorName: 'Johny.Cage', 11 | authorEmail: 'johny@cage.com', 12 | author: { 13 | id: '2', 14 | firstName: 'Johny', 15 | lastName: 'Cage', 16 | }, 17 | albums: [ 18 | { 19 | id: '1', 20 | authorName: 'Johny.Cage', 21 | authorEmail: 'johny@cage.com', 22 | name: 'My life', 23 | }, 24 | { 25 | id: '2', 26 | authorName: 'Johny.Cage', 27 | authorEmail: 'johny@cage.com', 28 | name: 'My young years', 29 | }, 30 | ], 31 | }; 32 | 33 | let photo = plainToClass(Photo, photoJson); 34 | console.log('deserialized object: ', photo); 35 | 36 | // now check serialization 37 | 38 | let newPhotoJson = classToPlain(photo); 39 | console.log('serialized object: ', newPhotoJson); 40 | 41 | // try to deserialize an array 42 | console.log('-------------------------------'); 43 | 44 | let photosJson = [ 45 | { 46 | id: '1', 47 | filename: 'myphoto.jpg', 48 | description: 'about my photo', 49 | author: { 50 | id: '2', 51 | firstName: 'Johny', 52 | lastName: 'Cage', 53 | registrationDate: '1995-12-17T03:24:00', 54 | }, 55 | albums: [ 56 | { 57 | id: '1', 58 | name: 'My life', 59 | }, 60 | { 61 | id: '2', 62 | name: 'My young years', 63 | }, 64 | ], 65 | }, 66 | { 67 | id: '2', 68 | filename: 'hisphoto.jpg', 69 | description: 'about his photo', 70 | author: { 71 | id: '2', 72 | firstName: 'Johny', 73 | lastName: 'Cage', 74 | }, 75 | albums: [ 76 | { 77 | id: '1', 78 | name: 'My life', 79 | }, 80 | { 81 | id: '2', 82 | name: 'My young years', 83 | }, 84 | ], 85 | }, 86 | ]; 87 | 88 | let photos = plainToClass(Photo, photosJson); 89 | console.log('deserialized array: ', photos); 90 | 91 | // now check array serialization 92 | 93 | let newPhotosJson = classToPlain(photos); 94 | console.log('serialized array: ', newPhotosJson); 95 | -------------------------------------------------------------------------------- /sample/sample3-custom-arrays/Album.ts: -------------------------------------------------------------------------------- 1 | export class Album { 2 | id: string; 3 | 4 | name: string; 5 | } 6 | -------------------------------------------------------------------------------- /sample/sample3-custom-arrays/AlbumArray.ts: -------------------------------------------------------------------------------- 1 | import { Album } from './Album'; 2 | 3 | export class AlbumArray extends Array { 4 | findByName(name: string) { 5 | return this.find(album => album.name === name); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sample/sample3-custom-arrays/Photo.ts: -------------------------------------------------------------------------------- 1 | import { Album } from './Album'; 2 | import { AlbumArray } from './AlbumArray'; 3 | import { Type } from '../../src/decorators'; 4 | 5 | export class Photo { 6 | id: string; 7 | 8 | filename: string; 9 | 10 | description: string; 11 | 12 | tags: string[]; 13 | 14 | @Type(() => Album) 15 | albums: AlbumArray; 16 | } 17 | -------------------------------------------------------------------------------- /sample/sample3-custom-arrays/app.ts: -------------------------------------------------------------------------------- 1 | import 'es6-shim'; 2 | import 'reflect-metadata'; 3 | import { classToPlain, plainToClass } from '../../src/index'; 4 | import { Photo } from './Photo'; 5 | 6 | // check deserialization 7 | 8 | let photoJson = { 9 | id: '1', 10 | filename: 'myphoto.jpg', 11 | description: 'about my photo', 12 | tags: ['me', 'iam'], 13 | albums: [ 14 | { 15 | id: '1', 16 | name: 'My life', 17 | }, 18 | { 19 | id: '2', 20 | name: 'My young years', 21 | }, 22 | ], 23 | }; 24 | 25 | let photo = plainToClass(Photo, photoJson); 26 | console.log('deserialized object: ', photo); 27 | console.log('-----------------------------'); 28 | console.log('Trying to find album: ', photo.albums.findByName('My life')); 29 | console.log('-----------------------------'); 30 | 31 | // now check serialization 32 | 33 | let newPhotoJson = classToPlain(photo); 34 | console.log('serialized object: ', newPhotoJson); 35 | console.log('-----------------------------'); 36 | -------------------------------------------------------------------------------- /sample/sample4-generics/SimpleCollection.ts: -------------------------------------------------------------------------------- 1 | export class SimpleCollection { 2 | items: T[]; 3 | count: number; 4 | } 5 | -------------------------------------------------------------------------------- /sample/sample4-generics/SuperCollection.ts: -------------------------------------------------------------------------------- 1 | import { Type, Exclude } from '../../src/decorators'; 2 | 3 | export class SuperCollection { 4 | @Exclude() 5 | private type: Function; 6 | 7 | @Type(options => { 8 | return (options.newObject as SuperCollection).type; 9 | }) 10 | items: T[]; 11 | 12 | count: number; 13 | 14 | constructor(type: Function) { 15 | this.type = type; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample/sample4-generics/User.ts: -------------------------------------------------------------------------------- 1 | import { Exclude } from '../../src/decorators'; 2 | 3 | export class User { 4 | id: number; 5 | 6 | firstName: string; 7 | 8 | lastName: string; 9 | 10 | @Exclude() 11 | password: string; 12 | 13 | constructor(id: number, firstName: string, lastName: string, password: string) { 14 | this.id = id; 15 | this.firstName = firstName; 16 | this.lastName = lastName; 17 | this.password = password; 18 | } 19 | 20 | get name() { 21 | return this.firstName + ' ' + this.lastName; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/sample4-generics/app.ts: -------------------------------------------------------------------------------- 1 | import 'es6-shim'; 2 | import 'reflect-metadata'; 3 | import { SimpleCollection } from './SimpleCollection'; 4 | import { User } from './User'; 5 | import { classToPlain, plainToClass, plainToClassFromExist } from '../../src/index'; 6 | import { SuperCollection } from './SuperCollection'; 7 | 8 | let collection = new SimpleCollection(); 9 | collection.items = [new User(1, 'Johny', 'Cage', '*******'), new User(2, 'Dima', 'Cage', '*******')]; 10 | collection.count = 2; 11 | 12 | // using generics works only for classToPlain operations, since in runtime we can 13 | // "guess" type without type provided only we have a constructor, not plain object. 14 | 15 | // console.log(classToPlain(collection)); 16 | 17 | // alternatively you can use factory method 18 | 19 | let collectionJson = { 20 | items: [ 21 | { 22 | id: 1, 23 | firstName: 'Johny', 24 | lastName: 'Cage', 25 | password: '*******', 26 | }, 27 | { 28 | id: 2, 29 | firstName: 'Dima', 30 | lastName: 'Cage', 31 | password: '*******', 32 | }, 33 | ], 34 | }; 35 | 36 | console.log(plainToClassFromExist(new SuperCollection(User), collectionJson)); 37 | -------------------------------------------------------------------------------- /sample/sample5-custom-transformer/User.ts: -------------------------------------------------------------------------------- 1 | import { Type, Transform } from '../../src/decorators'; 2 | import * as moment from 'moment'; 3 | 4 | export class User { 5 | id: number; 6 | 7 | name: string; 8 | 9 | @Type(() => Date) 10 | @Transform(value => value.toString(), { toPlainOnly: true }) 11 | @Transform(value => moment(value), { toClassOnly: true }) 12 | date: Date; 13 | } 14 | -------------------------------------------------------------------------------- /sample/sample5-custom-transformer/app.ts: -------------------------------------------------------------------------------- 1 | import 'es6-shim'; 2 | import 'reflect-metadata'; 3 | import { plainToClass, classToPlain } from '../../src/index'; 4 | import { User } from './User'; 5 | 6 | let userJson = { 7 | id: 1, 8 | name: 'Johny Cage', 9 | date: new Date().valueOf(), 10 | }; 11 | 12 | console.log(plainToClass(User, userJson)); 13 | 14 | const user = new User(); 15 | user.id = 1; 16 | user.name = 'Johny Cage'; 17 | user.date = new Date(); 18 | 19 | console.log(classToPlain(user)); 20 | -------------------------------------------------------------------------------- /src/ClassTransformer.ts: -------------------------------------------------------------------------------- 1 | import { ClassTransformOptions } from './interfaces'; 2 | import { TransformOperationExecutor } from './TransformOperationExecutor'; 3 | import { TransformationType } from './enums'; 4 | import { ClassConstructor } from './interfaces'; 5 | import { defaultOptions } from './constants/default-options.constant'; 6 | 7 | export class ClassTransformer { 8 | // ------------------------------------------------------------------------- 9 | // Public Methods 10 | // ------------------------------------------------------------------------- 11 | 12 | /** 13 | * Converts class (constructor) object to plain (literal) object. Also works with arrays. 14 | */ 15 | instanceToPlain>(object: T, options?: ClassTransformOptions): Record; 16 | instanceToPlain>(object: T[], options?: ClassTransformOptions): Record[]; 17 | instanceToPlain>( 18 | object: T | T[], 19 | options?: ClassTransformOptions 20 | ): Record | Record[] { 21 | const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_PLAIN, { 22 | ...defaultOptions, 23 | ...options, 24 | }); 25 | return executor.transform(undefined, object, undefined, undefined, undefined, undefined); 26 | } 27 | 28 | /** 29 | * Converts class (constructor) object to plain (literal) object. 30 | * Uses given plain object as source object (it means fills given plain object with data from class object). 31 | * Also works with arrays. 32 | */ 33 | classToPlainFromExist, P>( 34 | object: T, 35 | plainObject: P, 36 | options?: ClassTransformOptions 37 | ): T; 38 | classToPlainFromExist, P>( 39 | object: T, 40 | plainObjects: P[], 41 | options?: ClassTransformOptions 42 | ): T[]; 43 | classToPlainFromExist, P>( 44 | object: T, 45 | plainObject: P | P[], 46 | options?: ClassTransformOptions 47 | ): T | T[] { 48 | const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_PLAIN, { 49 | ...defaultOptions, 50 | ...options, 51 | }); 52 | return executor.transform(plainObject, object, undefined, undefined, undefined, undefined); 53 | } 54 | 55 | /** 56 | * Converts plain (literal) object to class (constructor) object. Also works with arrays. 57 | */ 58 | plainToInstance, V extends Array>( 59 | cls: ClassConstructor, 60 | plain: V, 61 | options?: ClassTransformOptions 62 | ): T[]; 63 | plainToInstance, V>( 64 | cls: ClassConstructor, 65 | plain: V, 66 | options?: ClassTransformOptions 67 | ): T; 68 | plainToInstance, V>( 69 | cls: ClassConstructor, 70 | plain: V | V[], 71 | options?: ClassTransformOptions 72 | ): T | T[] { 73 | const executor = new TransformOperationExecutor(TransformationType.PLAIN_TO_CLASS, { 74 | ...defaultOptions, 75 | ...options, 76 | }); 77 | return executor.transform(undefined, plain, cls, undefined, undefined, undefined); 78 | } 79 | 80 | /** 81 | * Converts plain (literal) object to class (constructor) object. 82 | * Uses given object as source object (it means fills given object with data from plain object). 83 | * Also works with arrays. 84 | */ 85 | plainToClassFromExist, V extends Array>( 86 | clsObject: T, 87 | plain: V, 88 | options?: ClassTransformOptions 89 | ): T; 90 | plainToClassFromExist, V>(clsObject: T, plain: V, options?: ClassTransformOptions): T[]; 91 | plainToClassFromExist, V>( 92 | clsObject: T, 93 | plain: V | V[], 94 | options?: ClassTransformOptions 95 | ): T | T[] { 96 | const executor = new TransformOperationExecutor(TransformationType.PLAIN_TO_CLASS, { 97 | ...defaultOptions, 98 | ...options, 99 | }); 100 | return executor.transform(clsObject, plain, undefined, undefined, undefined, undefined); 101 | } 102 | 103 | /** 104 | * Converts class (constructor) object to new class (constructor) object. Also works with arrays. 105 | */ 106 | instanceToInstance(object: T, options?: ClassTransformOptions): T; 107 | instanceToInstance(object: T[], options?: ClassTransformOptions): T[]; 108 | instanceToInstance(object: T | T[], options?: ClassTransformOptions): T | T[] { 109 | const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_CLASS, { 110 | ...defaultOptions, 111 | ...options, 112 | }); 113 | return executor.transform(undefined, object, undefined, undefined, undefined, undefined); 114 | } 115 | 116 | /** 117 | * Converts class (constructor) object to plain (literal) object. 118 | * Uses given plain object as source object (it means fills given plain object with data from class object). 119 | * Also works with arrays. 120 | */ 121 | classToClassFromExist(object: T, fromObject: T, options?: ClassTransformOptions): T; 122 | classToClassFromExist(object: T, fromObjects: T[], options?: ClassTransformOptions): T[]; 123 | classToClassFromExist(object: T, fromObject: T | T[], options?: ClassTransformOptions): T | T[] { 124 | const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_CLASS, { 125 | ...defaultOptions, 126 | ...options, 127 | }); 128 | return executor.transform(fromObject, object, undefined, undefined, undefined, undefined); 129 | } 130 | 131 | /** 132 | * Serializes given object to a JSON string. 133 | */ 134 | serialize(object: T, options?: ClassTransformOptions): string; 135 | serialize(object: T[], options?: ClassTransformOptions): string; 136 | serialize(object: T | T[], options?: ClassTransformOptions): string { 137 | return JSON.stringify(this.instanceToPlain(object, options)); 138 | } 139 | 140 | /** 141 | * Deserializes given JSON string to a object of the given class. 142 | */ 143 | deserialize(cls: ClassConstructor, json: string, options?: ClassTransformOptions): T { 144 | const jsonObject: T = JSON.parse(json); 145 | return this.plainToInstance(cls, jsonObject, options); 146 | } 147 | 148 | /** 149 | * Deserializes given JSON string to an array of objects of the given class. 150 | */ 151 | deserializeArray(cls: ClassConstructor, json: string, options?: ClassTransformOptions): T[] { 152 | const jsonObject: any[] = JSON.parse(json); 153 | return this.plainToInstance(cls, jsonObject, options); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/MetadataStorage.ts: -------------------------------------------------------------------------------- 1 | import { TypeMetadata, ExposeMetadata, ExcludeMetadata, TransformMetadata } from './interfaces'; 2 | import { TransformationType } from './enums'; 3 | 4 | /** 5 | * Storage all library metadata. 6 | */ 7 | export class MetadataStorage { 8 | // ------------------------------------------------------------------------- 9 | // Properties 10 | // ------------------------------------------------------------------------- 11 | 12 | private _typeMetadatas = new Map>(); 13 | private _transformMetadatas = new Map>(); 14 | private _exposeMetadatas = new Map>(); 15 | private _excludeMetadatas = new Map>(); 16 | private _ancestorsMap = new Map(); 17 | 18 | // ------------------------------------------------------------------------- 19 | // Adder Methods 20 | // ------------------------------------------------------------------------- 21 | 22 | addTypeMetadata(metadata: TypeMetadata): void { 23 | if (!this._typeMetadatas.has(metadata.target)) { 24 | this._typeMetadatas.set(metadata.target, new Map()); 25 | } 26 | this._typeMetadatas.get(metadata.target).set(metadata.propertyName, metadata); 27 | } 28 | 29 | addTransformMetadata(metadata: TransformMetadata): void { 30 | if (!this._transformMetadatas.has(metadata.target)) { 31 | this._transformMetadatas.set(metadata.target, new Map()); 32 | } 33 | if (!this._transformMetadatas.get(metadata.target).has(metadata.propertyName)) { 34 | this._transformMetadatas.get(metadata.target).set(metadata.propertyName, []); 35 | } 36 | this._transformMetadatas.get(metadata.target).get(metadata.propertyName).push(metadata); 37 | } 38 | 39 | addExposeMetadata(metadata: ExposeMetadata): void { 40 | if (!this._exposeMetadatas.has(metadata.target)) { 41 | this._exposeMetadatas.set(metadata.target, new Map()); 42 | } 43 | this._exposeMetadatas.get(metadata.target).set(metadata.propertyName, metadata); 44 | } 45 | 46 | addExcludeMetadata(metadata: ExcludeMetadata): void { 47 | if (!this._excludeMetadatas.has(metadata.target)) { 48 | this._excludeMetadatas.set(metadata.target, new Map()); 49 | } 50 | this._excludeMetadatas.get(metadata.target).set(metadata.propertyName, metadata); 51 | } 52 | 53 | // ------------------------------------------------------------------------- 54 | // Public Methods 55 | // ------------------------------------------------------------------------- 56 | 57 | findTransformMetadatas( 58 | target: Function, 59 | propertyName: string, 60 | transformationType: TransformationType 61 | ): TransformMetadata[] { 62 | return this.findMetadatas(this._transformMetadatas, target, propertyName).filter(metadata => { 63 | if (!metadata.options) return true; 64 | if (metadata.options.toClassOnly === true && metadata.options.toPlainOnly === true) return true; 65 | 66 | if (metadata.options.toClassOnly === true) { 67 | return ( 68 | transformationType === TransformationType.CLASS_TO_CLASS || 69 | transformationType === TransformationType.PLAIN_TO_CLASS 70 | ); 71 | } 72 | if (metadata.options.toPlainOnly === true) { 73 | return transformationType === TransformationType.CLASS_TO_PLAIN; 74 | } 75 | 76 | return true; 77 | }); 78 | } 79 | 80 | findExcludeMetadata(target: Function, propertyName: string): ExcludeMetadata { 81 | return this.findMetadata(this._excludeMetadatas, target, propertyName); 82 | } 83 | 84 | findExposeMetadata(target: Function, propertyName: string): ExposeMetadata { 85 | return this.findMetadata(this._exposeMetadatas, target, propertyName); 86 | } 87 | 88 | findExposeMetadataByCustomName(target: Function, name: string): ExposeMetadata { 89 | return this.getExposedMetadatas(target).find(metadata => { 90 | return metadata.options && metadata.options.name === name; 91 | }); 92 | } 93 | 94 | findTypeMetadata(target: Function, propertyName: string): TypeMetadata { 95 | return this.findMetadata(this._typeMetadatas, target, propertyName); 96 | } 97 | 98 | getStrategy(target: Function): 'excludeAll' | 'exposeAll' | 'none' { 99 | const excludeMap = this._excludeMetadatas.get(target); 100 | const exclude = excludeMap && excludeMap.get(undefined); 101 | const exposeMap = this._exposeMetadatas.get(target); 102 | const expose = exposeMap && exposeMap.get(undefined); 103 | if ((exclude && expose) || (!exclude && !expose)) return 'none'; 104 | return exclude ? 'excludeAll' : 'exposeAll'; 105 | } 106 | 107 | getExposedMetadatas(target: Function): ExposeMetadata[] { 108 | return this.getMetadata(this._exposeMetadatas, target); 109 | } 110 | 111 | getExcludedMetadatas(target: Function): ExcludeMetadata[] { 112 | return this.getMetadata(this._excludeMetadatas, target); 113 | } 114 | 115 | getExposedProperties(target: Function, transformationType: TransformationType): string[] { 116 | return this.getExposedMetadatas(target) 117 | .filter(metadata => { 118 | if (!metadata.options) return true; 119 | if (metadata.options.toClassOnly === true && metadata.options.toPlainOnly === true) return true; 120 | 121 | if (metadata.options.toClassOnly === true) { 122 | return ( 123 | transformationType === TransformationType.CLASS_TO_CLASS || 124 | transformationType === TransformationType.PLAIN_TO_CLASS 125 | ); 126 | } 127 | if (metadata.options.toPlainOnly === true) { 128 | return transformationType === TransformationType.CLASS_TO_PLAIN; 129 | } 130 | 131 | return true; 132 | }) 133 | .map(metadata => metadata.propertyName); 134 | } 135 | 136 | getExcludedProperties(target: Function, transformationType: TransformationType): string[] { 137 | return this.getExcludedMetadatas(target) 138 | .filter(metadata => { 139 | if (!metadata.options) return true; 140 | if (metadata.options.toClassOnly === true && metadata.options.toPlainOnly === true) return true; 141 | 142 | if (metadata.options.toClassOnly === true) { 143 | return ( 144 | transformationType === TransformationType.CLASS_TO_CLASS || 145 | transformationType === TransformationType.PLAIN_TO_CLASS 146 | ); 147 | } 148 | if (metadata.options.toPlainOnly === true) { 149 | return transformationType === TransformationType.CLASS_TO_PLAIN; 150 | } 151 | 152 | return true; 153 | }) 154 | .map(metadata => metadata.propertyName); 155 | } 156 | 157 | clear(): void { 158 | this._typeMetadatas.clear(); 159 | this._exposeMetadatas.clear(); 160 | this._excludeMetadatas.clear(); 161 | this._ancestorsMap.clear(); 162 | } 163 | 164 | // ------------------------------------------------------------------------- 165 | // Private Methods 166 | // ------------------------------------------------------------------------- 167 | 168 | private getMetadata( 169 | metadatas: Map>, 170 | target: Function 171 | ): T[] { 172 | const metadataFromTargetMap = metadatas.get(target); 173 | let metadataFromTarget: T[]; 174 | if (metadataFromTargetMap) { 175 | metadataFromTarget = Array.from(metadataFromTargetMap.values()).filter(meta => meta.propertyName !== undefined); 176 | } 177 | const metadataFromAncestors: T[] = []; 178 | for (const ancestor of this.getAncestors(target)) { 179 | const ancestorMetadataMap = metadatas.get(ancestor); 180 | if (ancestorMetadataMap) { 181 | const metadataFromAncestor = Array.from(ancestorMetadataMap.values()).filter( 182 | meta => meta.propertyName !== undefined 183 | ); 184 | metadataFromAncestors.push(...metadataFromAncestor); 185 | } 186 | } 187 | return metadataFromAncestors.concat(metadataFromTarget || []); 188 | } 189 | 190 | private findMetadata( 191 | metadatas: Map>, 192 | target: Function, 193 | propertyName: string 194 | ): T { 195 | const metadataFromTargetMap = metadatas.get(target); 196 | if (metadataFromTargetMap) { 197 | const metadataFromTarget = metadataFromTargetMap.get(propertyName); 198 | if (metadataFromTarget) { 199 | return metadataFromTarget; 200 | } 201 | } 202 | for (const ancestor of this.getAncestors(target)) { 203 | const ancestorMetadataMap = metadatas.get(ancestor); 204 | if (ancestorMetadataMap) { 205 | const ancestorResult = ancestorMetadataMap.get(propertyName); 206 | if (ancestorResult) { 207 | return ancestorResult; 208 | } 209 | } 210 | } 211 | return undefined; 212 | } 213 | 214 | private findMetadatas( 215 | metadatas: Map>, 216 | target: Function, 217 | propertyName: string 218 | ): T[] { 219 | const metadataFromTargetMap = metadatas.get(target); 220 | let metadataFromTarget: T[]; 221 | if (metadataFromTargetMap) { 222 | metadataFromTarget = metadataFromTargetMap.get(propertyName); 223 | } 224 | const metadataFromAncestorsTarget: T[] = []; 225 | for (const ancestor of this.getAncestors(target)) { 226 | const ancestorMetadataMap = metadatas.get(ancestor); 227 | if (ancestorMetadataMap) { 228 | if (ancestorMetadataMap.has(propertyName)) { 229 | metadataFromAncestorsTarget.push(...ancestorMetadataMap.get(propertyName)); 230 | } 231 | } 232 | } 233 | return metadataFromAncestorsTarget 234 | .slice() 235 | .reverse() 236 | .concat((metadataFromTarget || []).slice().reverse()); 237 | } 238 | 239 | private getAncestors(target: Function): Function[] { 240 | if (!target) return []; 241 | if (!this._ancestorsMap.has(target)) { 242 | const ancestors: Function[] = []; 243 | for ( 244 | let baseClass = Object.getPrototypeOf(target.prototype.constructor); 245 | typeof baseClass.prototype !== 'undefined'; 246 | baseClass = Object.getPrototypeOf(baseClass.prototype.constructor) 247 | ) { 248 | ancestors.push(baseClass); 249 | } 250 | this._ancestorsMap.set(target, ancestors); 251 | } 252 | return this._ancestorsMap.get(target); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/TransformOperationExecutor.ts: -------------------------------------------------------------------------------- 1 | import { defaultMetadataStorage } from './storage'; 2 | import { ClassTransformOptions, TypeHelpOptions, TypeMetadata, TypeOptions } from './interfaces'; 3 | import { TransformationType } from './enums'; 4 | import { getGlobal, isPromise } from './utils'; 5 | 6 | function instantiateArrayType(arrayType: Function): Array | Set { 7 | const array = new (arrayType as any)(); 8 | if (!(array instanceof Set) && !('push' in array)) { 9 | return []; 10 | } 11 | return array; 12 | } 13 | 14 | export class TransformOperationExecutor { 15 | // ------------------------------------------------------------------------- 16 | // Private Properties 17 | // ------------------------------------------------------------------------- 18 | 19 | private recursionStack = new Set>(); 20 | 21 | // ------------------------------------------------------------------------- 22 | // Constructor 23 | // ------------------------------------------------------------------------- 24 | 25 | constructor(private transformationType: TransformationType, private options: ClassTransformOptions) {} 26 | 27 | // ------------------------------------------------------------------------- 28 | // Public Methods 29 | // ------------------------------------------------------------------------- 30 | 31 | transform( 32 | source: Record | Record[] | any, 33 | value: Record | Record[] | any, 34 | targetType: Function | TypeMetadata, 35 | arrayType: Function, 36 | isMap: boolean, 37 | level: number = 0 38 | ): any { 39 | if (Array.isArray(value) || value instanceof Set) { 40 | const newValue = 41 | arrayType && this.transformationType === TransformationType.PLAIN_TO_CLASS 42 | ? instantiateArrayType(arrayType) 43 | : []; 44 | (value as any[]).forEach((subValue, index) => { 45 | const subSource = source ? source[index] : undefined; 46 | if (!this.options.enableCircularCheck || !this.isCircular(subValue)) { 47 | let realTargetType; 48 | if ( 49 | typeof targetType !== 'function' && 50 | targetType && 51 | targetType.options && 52 | targetType.options.discriminator && 53 | targetType.options.discriminator.property && 54 | targetType.options.discriminator.subTypes 55 | ) { 56 | if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { 57 | realTargetType = targetType.options.discriminator.subTypes.find( 58 | subType => 59 | subType.name === subValue[(targetType as { options: TypeOptions }).options.discriminator.property] 60 | ); 61 | const options: TypeHelpOptions = { newObject: newValue, object: subValue, property: undefined }; 62 | const newType = targetType.typeFunction(options); 63 | realTargetType === undefined ? (realTargetType = newType) : (realTargetType = realTargetType.value); 64 | if (!targetType.options.keepDiscriminatorProperty) 65 | delete subValue[targetType.options.discriminator.property]; 66 | } 67 | 68 | if (this.transformationType === TransformationType.CLASS_TO_CLASS) { 69 | realTargetType = subValue.constructor; 70 | } 71 | if (this.transformationType === TransformationType.CLASS_TO_PLAIN) { 72 | subValue[targetType.options.discriminator.property] = targetType.options.discriminator.subTypes.find( 73 | subType => subType.value === subValue.constructor 74 | ).name; 75 | } 76 | } else { 77 | realTargetType = targetType; 78 | } 79 | const value = this.transform( 80 | subSource, 81 | subValue, 82 | realTargetType, 83 | undefined, 84 | subValue instanceof Map, 85 | level + 1 86 | ); 87 | 88 | if (newValue instanceof Set) { 89 | newValue.add(value); 90 | } else { 91 | newValue.push(value); 92 | } 93 | } else if (this.transformationType === TransformationType.CLASS_TO_CLASS) { 94 | if (newValue instanceof Set) { 95 | newValue.add(subValue); 96 | } else { 97 | newValue.push(subValue); 98 | } 99 | } 100 | }); 101 | return newValue; 102 | } else if (targetType === String && !isMap) { 103 | if (value === null || value === undefined) return value; 104 | return String(value); 105 | } else if (targetType === Number && !isMap) { 106 | if (value === null || value === undefined) return value; 107 | return Number(value); 108 | } else if (targetType === Boolean && !isMap) { 109 | if (value === null || value === undefined) return value; 110 | return Boolean(value); 111 | } else if ((targetType === Date || value instanceof Date) && !isMap) { 112 | if (value instanceof Date) { 113 | return new Date(value.valueOf()); 114 | } 115 | if (value === null || value === undefined) return value; 116 | return new Date(value); 117 | } else if (!!getGlobal().Buffer && (targetType === Buffer || value instanceof Buffer) && !isMap) { 118 | if (value === null || value === undefined) return value; 119 | return Buffer.from(value); 120 | } else if (isPromise(value) && !isMap) { 121 | return new Promise((resolve, reject) => { 122 | value.then( 123 | (data: any) => resolve(this.transform(undefined, data, targetType, undefined, undefined, level + 1)), 124 | reject 125 | ); 126 | }); 127 | } else if (!isMap && value !== null && typeof value === 'object' && typeof value.then === 'function') { 128 | // Note: We should not enter this, as promise has been handled above 129 | // This option simply returns the Promise preventing a JS error from happening and should be an inaccessible path. 130 | return value; // skip promise transformation 131 | } else if (typeof value === 'object' && value !== null) { 132 | // try to guess the type 133 | if (!targetType && value.constructor !== Object /* && TransformationType === TransformationType.CLASS_TO_PLAIN*/) 134 | if (!Array.isArray(value) && value.constructor === Array) { 135 | // Somebody attempts to convert special Array like object to Array, eg: 136 | // const evilObject = { '100000000': '100000000', __proto__: [] }; 137 | // This could be used to cause Denial-of-service attack so we don't allow it. 138 | // See prevent-array-bomb.spec.ts for more details. 139 | } else { 140 | // We are good we can use the built-in constructor 141 | targetType = value.constructor; 142 | } 143 | if (!targetType && source) targetType = source.constructor; 144 | 145 | if (this.options.enableCircularCheck) { 146 | // add transformed type to prevent circular references 147 | this.recursionStack.add(value); 148 | } 149 | 150 | const keys = this.getKeys(targetType as Function, value, isMap); 151 | let newValue: any = source ? source : {}; 152 | if ( 153 | !source && 154 | (this.transformationType === TransformationType.PLAIN_TO_CLASS || 155 | this.transformationType === TransformationType.CLASS_TO_CLASS) 156 | ) { 157 | if (isMap) { 158 | newValue = new Map(); 159 | } else if (targetType) { 160 | newValue = new (targetType as any)(); 161 | } else { 162 | newValue = {}; 163 | } 164 | } 165 | 166 | // traverse over keys 167 | for (const key of keys) { 168 | if (key === '__proto__' || key === 'constructor') { 169 | continue; 170 | } 171 | 172 | const valueKey = key; 173 | let newValueKey = key, 174 | propertyName = key; 175 | if (!this.options.ignoreDecorators && targetType) { 176 | if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { 177 | const exposeMetadata = defaultMetadataStorage.findExposeMetadataByCustomName(targetType as Function, key); 178 | if (exposeMetadata) { 179 | propertyName = exposeMetadata.propertyName; 180 | newValueKey = exposeMetadata.propertyName; 181 | } 182 | } else if ( 183 | this.transformationType === TransformationType.CLASS_TO_PLAIN || 184 | this.transformationType === TransformationType.CLASS_TO_CLASS 185 | ) { 186 | const exposeMetadata = defaultMetadataStorage.findExposeMetadata(targetType as Function, key); 187 | if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name) { 188 | newValueKey = exposeMetadata.options.name; 189 | } 190 | } 191 | } 192 | 193 | // get a subvalue 194 | let subValue: any = undefined; 195 | if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { 196 | /** 197 | * This section is added for the following report: 198 | * https://github.com/typestack/class-transformer/issues/596 199 | * 200 | * We should not call functions or constructors when transforming to class. 201 | */ 202 | subValue = value[valueKey]; 203 | } else { 204 | if (value instanceof Map) { 205 | subValue = value.get(valueKey); 206 | } else if (value[valueKey] instanceof Function) { 207 | subValue = value[valueKey](); 208 | } else { 209 | subValue = value[valueKey]; 210 | } 211 | } 212 | 213 | // determine a type 214 | let type: any = undefined, 215 | isSubValueMap = subValue instanceof Map; 216 | if (targetType && isMap) { 217 | type = targetType; 218 | } else if (targetType) { 219 | const metadata = defaultMetadataStorage.findTypeMetadata(targetType as Function, propertyName); 220 | if (metadata) { 221 | const options: TypeHelpOptions = { newObject: newValue, object: value, property: propertyName }; 222 | const newType = metadata.typeFunction ? metadata.typeFunction(options) : metadata.reflectedType; 223 | if ( 224 | metadata.options && 225 | metadata.options.discriminator && 226 | metadata.options.discriminator.property && 227 | metadata.options.discriminator.subTypes 228 | ) { 229 | if (!(value[valueKey] instanceof Array)) { 230 | if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { 231 | type = metadata.options.discriminator.subTypes.find(subType => { 232 | if (subValue && subValue instanceof Object && metadata.options.discriminator.property in subValue) { 233 | return subType.name === subValue[metadata.options.discriminator.property]; 234 | } 235 | }); 236 | type === undefined ? (type = newType) : (type = type.value); 237 | if (!metadata.options.keepDiscriminatorProperty) { 238 | if (subValue && subValue instanceof Object && metadata.options.discriminator.property in subValue) { 239 | delete subValue[metadata.options.discriminator.property]; 240 | } 241 | } 242 | } 243 | if (this.transformationType === TransformationType.CLASS_TO_CLASS) { 244 | type = subValue.constructor; 245 | } 246 | if (this.transformationType === TransformationType.CLASS_TO_PLAIN) { 247 | if (subValue) { 248 | subValue[metadata.options.discriminator.property] = metadata.options.discriminator.subTypes.find( 249 | subType => subType.value === subValue.constructor 250 | ).name; 251 | } 252 | } 253 | } else { 254 | type = metadata; 255 | } 256 | } else { 257 | type = newType; 258 | } 259 | isSubValueMap = isSubValueMap || metadata.reflectedType === Map; 260 | } else if (this.options.targetMaps) { 261 | // try to find a type in target maps 262 | this.options.targetMaps 263 | .filter(map => map.target === targetType && !!map.properties[propertyName]) 264 | .forEach(map => (type = map.properties[propertyName])); 265 | } else if ( 266 | this.options.enableImplicitConversion && 267 | this.transformationType === TransformationType.PLAIN_TO_CLASS 268 | ) { 269 | // if we have no registererd type via the @Type() decorator then we check if we have any 270 | // type declarations in reflect-metadata (type declaration is emited only if some decorator is added to the property.) 271 | const reflectedType = (Reflect as any).getMetadata( 272 | 'design:type', 273 | (targetType as Function).prototype, 274 | propertyName 275 | ); 276 | 277 | if (reflectedType) { 278 | type = reflectedType; 279 | } 280 | } 281 | } 282 | 283 | // if value is an array try to get its custom array type 284 | const arrayType = Array.isArray(value[valueKey]) 285 | ? this.getReflectedType(targetType as Function, propertyName) 286 | : undefined; 287 | 288 | // const subValueKey = TransformationType === TransformationType.PLAIN_TO_CLASS && newKeyName ? newKeyName : key; 289 | const subSource = source ? source[valueKey] : undefined; 290 | 291 | // if its deserialization then type if required 292 | // if we uncomment this types like string[] will not work 293 | // if (this.transformationType === TransformationType.PLAIN_TO_CLASS && !type && subValue instanceof Object && !(subValue instanceof Date)) 294 | // throw new Error(`Cannot determine type for ${(targetType as any).name }.${propertyName}, did you forget to specify a @Type?`); 295 | 296 | // if newValue is a source object that has method that match newKeyName then skip it 297 | if (newValue.constructor.prototype) { 298 | const descriptor = this.getPropertyDescriptor(newValue.constructor.prototype, newValueKey); 299 | if ( 300 | (this.transformationType === TransformationType.PLAIN_TO_CLASS || 301 | this.transformationType === TransformationType.CLASS_TO_CLASS) && 302 | // eslint-disable-next-line @typescript-eslint/unbound-method 303 | ((descriptor && !descriptor.set) || newValue[newValueKey] instanceof Function) 304 | ) 305 | // || TransformationType === TransformationType.CLASS_TO_CLASS 306 | continue; 307 | } 308 | 309 | if (!this.options.enableCircularCheck || !this.isCircular(subValue)) { 310 | const transformKey = this.transformationType === TransformationType.PLAIN_TO_CLASS ? newValueKey : key; 311 | let finalValue; 312 | 313 | if (this.transformationType === TransformationType.CLASS_TO_PLAIN) { 314 | // Get original value 315 | finalValue = value[transformKey]; 316 | // Apply custom transformation 317 | finalValue = this.applyCustomTransformations( 318 | finalValue, 319 | targetType as Function, 320 | transformKey, 321 | value, 322 | this.transformationType 323 | ); 324 | // If nothing change, it means no custom transformation was applied, so use the subValue. 325 | finalValue = value[transformKey] === finalValue ? subValue : finalValue; 326 | // Apply the default transformation 327 | finalValue = this.transform(subSource, finalValue, type, arrayType, isSubValueMap, level + 1); 328 | } else { 329 | if (subValue === undefined && this.options.exposeDefaultValues) { 330 | // Set default value if nothing provided 331 | finalValue = newValue[newValueKey]; 332 | } else { 333 | finalValue = this.transform(subSource, subValue, type, arrayType, isSubValueMap, level + 1); 334 | finalValue = this.applyCustomTransformations( 335 | finalValue, 336 | targetType as Function, 337 | transformKey, 338 | value, 339 | this.transformationType 340 | ); 341 | } 342 | } 343 | 344 | if (finalValue !== undefined || this.options.exposeUnsetFields) { 345 | if (newValue instanceof Map) { 346 | newValue.set(newValueKey, finalValue); 347 | } else { 348 | newValue[newValueKey] = finalValue; 349 | } 350 | } 351 | } else if (this.transformationType === TransformationType.CLASS_TO_CLASS) { 352 | let finalValue = subValue; 353 | finalValue = this.applyCustomTransformations( 354 | finalValue, 355 | targetType as Function, 356 | key, 357 | value, 358 | this.transformationType 359 | ); 360 | if (finalValue !== undefined || this.options.exposeUnsetFields) { 361 | if (newValue instanceof Map) { 362 | newValue.set(newValueKey, finalValue); 363 | } else { 364 | newValue[newValueKey] = finalValue; 365 | } 366 | } 367 | } 368 | } 369 | 370 | if (this.options.enableCircularCheck) { 371 | this.recursionStack.delete(value); 372 | } 373 | 374 | return newValue; 375 | } else { 376 | return value; 377 | } 378 | } 379 | 380 | private applyCustomTransformations( 381 | value: any, 382 | target: Function, 383 | key: string, 384 | obj: any, 385 | transformationType: TransformationType 386 | ): boolean { 387 | let metadatas = defaultMetadataStorage.findTransformMetadatas(target, key, this.transformationType); 388 | 389 | // apply versioning options 390 | if (this.options.version !== undefined) { 391 | metadatas = metadatas.filter(metadata => { 392 | if (!metadata.options) return true; 393 | 394 | return this.checkVersion(metadata.options.since, metadata.options.until); 395 | }); 396 | } 397 | 398 | // apply grouping options 399 | if (this.options.groups && this.options.groups.length) { 400 | metadatas = metadatas.filter(metadata => { 401 | if (!metadata.options) return true; 402 | 403 | return this.checkGroups(metadata.options.groups); 404 | }); 405 | } else { 406 | metadatas = metadatas.filter(metadata => { 407 | return !metadata.options || !metadata.options.groups || !metadata.options.groups.length; 408 | }); 409 | } 410 | 411 | metadatas.forEach(metadata => { 412 | value = metadata.transformFn({ value, key, obj, type: transformationType, options: this.options }); 413 | }); 414 | 415 | return value; 416 | } 417 | 418 | // preventing circular references 419 | private isCircular(object: Record): boolean { 420 | return this.recursionStack.has(object); 421 | } 422 | 423 | private getReflectedType(target: Function, propertyName: string): Function | undefined { 424 | if (!target) return undefined; 425 | const meta = defaultMetadataStorage.findTypeMetadata(target, propertyName); 426 | return meta ? meta.reflectedType : undefined; 427 | } 428 | 429 | private getKeys(target: Function, object: Record, isMap: boolean): string[] { 430 | // determine exclusion strategy 431 | let strategy = defaultMetadataStorage.getStrategy(target); 432 | if (strategy === 'none') strategy = this.options.strategy || 'exposeAll'; // exposeAll is default strategy 433 | 434 | // get all keys that need to expose 435 | let keys: any[] = []; 436 | if (strategy === 'exposeAll' || isMap) { 437 | if (object instanceof Map) { 438 | keys = Array.from(object.keys()); 439 | } else { 440 | keys = Object.keys(object); 441 | } 442 | } 443 | 444 | if (isMap) { 445 | // expose & exclude do not apply for map keys only to fields 446 | return keys; 447 | } 448 | 449 | /** 450 | * If decorators are ignored but we don't want the extraneous values, then we use the 451 | * metadata to decide which property is needed, but doesn't apply the decorator effect. 452 | */ 453 | if (this.options.ignoreDecorators && this.options.excludeExtraneousValues && target) { 454 | const exposedProperties = defaultMetadataStorage.getExposedProperties(target, this.transformationType); 455 | const excludedProperties = defaultMetadataStorage.getExcludedProperties(target, this.transformationType); 456 | keys = [...exposedProperties, ...excludedProperties]; 457 | } 458 | 459 | if (!this.options.ignoreDecorators && target) { 460 | // add all exposed to list of keys 461 | let exposedProperties = defaultMetadataStorage.getExposedProperties(target, this.transformationType); 462 | if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { 463 | exposedProperties = exposedProperties.map(key => { 464 | const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key); 465 | if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name) { 466 | return exposeMetadata.options.name; 467 | } 468 | 469 | return key; 470 | }); 471 | } 472 | if (this.options.excludeExtraneousValues) { 473 | keys = exposedProperties; 474 | } else { 475 | keys = keys.concat(exposedProperties); 476 | } 477 | 478 | // exclude excluded properties 479 | const excludedProperties = defaultMetadataStorage.getExcludedProperties(target, this.transformationType); 480 | if (excludedProperties.length > 0) { 481 | keys = keys.filter(key => { 482 | return !excludedProperties.includes(key); 483 | }); 484 | } 485 | 486 | // apply versioning options 487 | if (this.options.version !== undefined) { 488 | keys = keys.filter(key => { 489 | const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key); 490 | if (!exposeMetadata || !exposeMetadata.options) return true; 491 | 492 | return this.checkVersion(exposeMetadata.options.since, exposeMetadata.options.until); 493 | }); 494 | } 495 | 496 | // apply grouping options 497 | if (this.options.groups && this.options.groups.length) { 498 | keys = keys.filter(key => { 499 | const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key); 500 | if (!exposeMetadata || !exposeMetadata.options) return true; 501 | 502 | return this.checkGroups(exposeMetadata.options.groups); 503 | }); 504 | } else { 505 | keys = keys.filter(key => { 506 | const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key); 507 | return ( 508 | !exposeMetadata || 509 | !exposeMetadata.options || 510 | !exposeMetadata.options.groups || 511 | !exposeMetadata.options.groups.length 512 | ); 513 | }); 514 | } 515 | } 516 | 517 | // exclude prefixed properties 518 | if (this.options.excludePrefixes && this.options.excludePrefixes.length) { 519 | keys = keys.filter(key => 520 | this.options.excludePrefixes.every(prefix => { 521 | return key.substr(0, prefix.length) !== prefix; 522 | }) 523 | ); 524 | } 525 | 526 | // make sure we have unique keys 527 | keys = keys.filter((key, index, self) => { 528 | return self.indexOf(key) === index; 529 | }); 530 | 531 | return keys; 532 | } 533 | 534 | private checkVersion(since: number, until: number): boolean { 535 | let decision = true; 536 | if (decision && since) decision = this.options.version >= since; 537 | if (decision && until) decision = this.options.version < until; 538 | 539 | return decision; 540 | } 541 | 542 | private checkGroups(groups: string[]): boolean { 543 | if (!groups) return true; 544 | 545 | return this.options.groups.some(optionGroup => groups.includes(optionGroup)); 546 | } 547 | 548 | private getPropertyDescriptor(obj: any, key: PropertyKey): PropertyDescriptor | undefined { 549 | const descriptor = Object.getOwnPropertyDescriptor(obj, key); 550 | if (descriptor) return descriptor; 551 | 552 | const prototype = Object.getPrototypeOf(obj); 553 | return prototype ? this.getPropertyDescriptor(prototype, key) : undefined; 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /src/constants/default-options.constant.ts: -------------------------------------------------------------------------------- 1 | import { ClassTransformOptions } from '../interfaces/class-transformer-options.interface'; 2 | 3 | /** 4 | * These are the default options used by any transformation operation. 5 | */ 6 | export const defaultOptions: Partial = { 7 | enableCircularCheck: false, 8 | enableImplicitConversion: false, 9 | excludeExtraneousValues: false, 10 | excludePrefixes: undefined, 11 | exposeDefaultValues: false, 12 | exposeUnsetFields: true, 13 | groups: undefined, 14 | ignoreDecorators: false, 15 | strategy: undefined, 16 | targetMaps: undefined, 17 | version: undefined, 18 | }; 19 | -------------------------------------------------------------------------------- /src/decorators/exclude.decorator.ts: -------------------------------------------------------------------------------- 1 | import { defaultMetadataStorage } from '../storage'; 2 | import { ExcludeOptions } from '../interfaces'; 3 | 4 | /** 5 | * Marks the given class or property as excluded. By default the property is excluded in both 6 | * constructorToPlain and plainToConstructor transformations. It can be limited to only one direction 7 | * via using the `toPlainOnly` or `toClassOnly` option. 8 | * 9 | * Can be applied to class definitions and properties. 10 | */ 11 | export function Exclude(options: ExcludeOptions = {}): PropertyDecorator & ClassDecorator { 12 | /** 13 | * NOTE: The `propertyName` property must be marked as optional because 14 | * this decorator used both as a class and a property decorator and the 15 | * Typescript compiler will freak out if we make it mandatory as a class 16 | * decorator only receives one parameter. 17 | */ 18 | return function (object: any, propertyName?: string | Symbol): void { 19 | defaultMetadataStorage.addExcludeMetadata({ 20 | target: object instanceof Function ? object : object.constructor, 21 | propertyName: propertyName as string, 22 | options, 23 | }); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/decorators/expose.decorator.ts: -------------------------------------------------------------------------------- 1 | import { defaultMetadataStorage } from '../storage'; 2 | import { ExposeOptions } from '../interfaces'; 3 | 4 | /** 5 | * Marks the given class or property as included. By default the property is included in both 6 | * constructorToPlain and plainToConstructor transformations. It can be limited to only one direction 7 | * via using the `toPlainOnly` or `toClassOnly` option. 8 | * 9 | * Can be applied to class definitions and properties. 10 | */ 11 | export function Expose(options: ExposeOptions = {}): PropertyDecorator & ClassDecorator { 12 | /** 13 | * NOTE: The `propertyName` property must be marked as optional because 14 | * this decorator used both as a class and a property decorator and the 15 | * Typescript compiler will freak out if we make it mandatory as a class 16 | * decorator only receives one parameter. 17 | */ 18 | return function (object: any, propertyName?: string | Symbol): void { 19 | defaultMetadataStorage.addExposeMetadata({ 20 | target: object instanceof Function ? object : object.constructor, 21 | propertyName: propertyName as string, 22 | options, 23 | }); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './exclude.decorator'; 2 | export * from './expose.decorator'; 3 | export * from './transform-instance-to-instance.decorator'; 4 | export * from './transform-instance-to-plain.decorator'; 5 | export * from './transform-plain-to-instance.decorator'; 6 | export * from './transform.decorator'; 7 | export * from './type.decorator'; 8 | -------------------------------------------------------------------------------- /src/decorators/transform-instance-to-instance.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ClassTransformer } from '../ClassTransformer'; 2 | import { ClassTransformOptions } from '../interfaces'; 3 | 4 | /** 5 | * Return the class instance only with the exposed properties. 6 | * 7 | * Can be applied to functions and getters/setters only. 8 | */ 9 | export function TransformInstanceToInstance(params?: ClassTransformOptions): MethodDecorator { 10 | return function (target: Record, propertyKey: string | Symbol, descriptor: PropertyDescriptor): void { 11 | const classTransformer: ClassTransformer = new ClassTransformer(); 12 | const originalMethod = descriptor.value; 13 | 14 | descriptor.value = function (...args: any[]): Record { 15 | const result: any = originalMethod.apply(this, args); 16 | const isPromise = 17 | !!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function'; 18 | return isPromise 19 | ? result.then((data: any) => classTransformer.instanceToInstance(data, params)) 20 | : classTransformer.instanceToInstance(result, params); 21 | }; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/decorators/transform-instance-to-plain.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ClassTransformer } from '../ClassTransformer'; 2 | import { ClassTransformOptions } from '../interfaces'; 3 | 4 | /** 5 | * Transform the object from class to plain object and return only with the exposed properties. 6 | * 7 | * Can be applied to functions and getters/setters only. 8 | */ 9 | export function TransformInstanceToPlain(params?: ClassTransformOptions): MethodDecorator { 10 | return function (target: Record, propertyKey: string | Symbol, descriptor: PropertyDescriptor): void { 11 | const classTransformer: ClassTransformer = new ClassTransformer(); 12 | const originalMethod = descriptor.value; 13 | 14 | descriptor.value = function (...args: any[]): Record { 15 | const result: any = originalMethod.apply(this, args); 16 | const isPromise = 17 | !!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function'; 18 | return isPromise 19 | ? result.then((data: any) => classTransformer.instanceToPlain(data, params)) 20 | : classTransformer.instanceToPlain(result, params); 21 | }; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/decorators/transform-plain-to-instance.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ClassTransformer } from '../ClassTransformer'; 2 | import { ClassTransformOptions, ClassConstructor } from '../interfaces'; 3 | 4 | /** 5 | * Return the class instance only with the exposed properties. 6 | * 7 | * Can be applied to functions and getters/setters only. 8 | */ 9 | export function TransformPlainToInstance( 10 | classType: ClassConstructor, 11 | params?: ClassTransformOptions 12 | ): MethodDecorator { 13 | return function (target: Record, propertyKey: string | Symbol, descriptor: PropertyDescriptor): void { 14 | const classTransformer: ClassTransformer = new ClassTransformer(); 15 | const originalMethod = descriptor.value; 16 | 17 | descriptor.value = function (...args: any[]): Record { 18 | const result: any = originalMethod.apply(this, args); 19 | const isPromise = 20 | !!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function'; 21 | return isPromise 22 | ? result.then((data: any) => classTransformer.plainToInstance(classType, data, params)) 23 | : classTransformer.plainToInstance(classType, result, params); 24 | }; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/decorators/transform.decorator.ts: -------------------------------------------------------------------------------- 1 | import { defaultMetadataStorage } from '../storage'; 2 | import { TransformFnParams, TransformOptions } from '../interfaces'; 3 | 4 | /** 5 | * Defines a custom logic for value transformation. 6 | * 7 | * Can be applied to properties only. 8 | */ 9 | export function Transform( 10 | transformFn: (params: TransformFnParams) => any, 11 | options: TransformOptions = {} 12 | ): PropertyDecorator { 13 | return function (target: any, propertyName: string | Symbol): void { 14 | defaultMetadataStorage.addTransformMetadata({ 15 | target: target.constructor, 16 | propertyName: propertyName as string, 17 | transformFn, 18 | options, 19 | }); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/decorators/type.decorator.ts: -------------------------------------------------------------------------------- 1 | import { defaultMetadataStorage } from '../storage'; 2 | import { TypeHelpOptions, TypeOptions } from '../interfaces'; 3 | 4 | /** 5 | * Specifies a type of the property. 6 | * The given TypeFunction can return a constructor. A discriminator can be given in the options. 7 | * 8 | * Can be applied to properties only. 9 | */ 10 | export function Type( 11 | typeFunction?: (type?: TypeHelpOptions) => Function, 12 | options: TypeOptions = {} 13 | ): PropertyDecorator { 14 | return function (target: any, propertyName: string | Symbol): void { 15 | const reflectedType = (Reflect as any).getMetadata('design:type', target, propertyName); 16 | defaultMetadataStorage.addTypeMetadata({ 17 | target: target.constructor, 18 | propertyName: propertyName as string, 19 | reflectedType, 20 | typeFunction, 21 | options, 22 | }); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transformation-type.enum'; 2 | -------------------------------------------------------------------------------- /src/enums/transformation-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum TransformationType { 2 | PLAIN_TO_CLASS, 3 | CLASS_TO_PLAIN, 4 | CLASS_TO_CLASS, 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ClassTransformer } from './ClassTransformer'; 2 | import { ClassTransformOptions } from './interfaces'; 3 | import { ClassConstructor } from './interfaces'; 4 | 5 | export { ClassTransformer } from './ClassTransformer'; 6 | export * from './decorators'; 7 | export * from './interfaces'; 8 | export * from './enums'; 9 | 10 | const classTransformer = new ClassTransformer(); 11 | 12 | /** 13 | * Converts class (constructor) object to plain (literal) object. Also works with arrays. 14 | * 15 | * @deprecated Function name changed, use the `instanceToPlain` method instead. 16 | */ 17 | export function classToPlain(object: T, options?: ClassTransformOptions): Record; 18 | export function classToPlain(object: T[], options?: ClassTransformOptions): Record[]; 19 | export function classToPlain( 20 | object: T | T[], 21 | options?: ClassTransformOptions 22 | ): Record | Record[] { 23 | return classTransformer.instanceToPlain(object, options); 24 | } 25 | 26 | /** 27 | * Converts class (constructor) object to plain (literal) object. Also works with arrays. 28 | */ 29 | export function instanceToPlain(object: T, options?: ClassTransformOptions): Record; 30 | export function instanceToPlain(object: T[], options?: ClassTransformOptions): Record[]; 31 | export function instanceToPlain( 32 | object: T | T[], 33 | options?: ClassTransformOptions 34 | ): Record | Record[] { 35 | return classTransformer.instanceToPlain(object, options); 36 | } 37 | 38 | /** 39 | * Converts class (constructor) object to plain (literal) object. 40 | * Uses given plain object as source object (it means fills given plain object with data from class object). 41 | * Also works with arrays. 42 | * 43 | * @deprecated This function is being removed. 44 | */ 45 | export function classToPlainFromExist( 46 | object: T, 47 | plainObject: Record, 48 | options?: ClassTransformOptions 49 | ): Record; 50 | export function classToPlainFromExist( 51 | object: T, 52 | plainObjects: Record[], 53 | options?: ClassTransformOptions 54 | ): Record[]; 55 | export function classToPlainFromExist( 56 | object: T, 57 | plainObject: Record | Record[], 58 | options?: ClassTransformOptions 59 | ): Record | Record[] { 60 | return classTransformer.classToPlainFromExist(object, plainObject, options); 61 | } 62 | 63 | /** 64 | * Converts plain (literal) object to class (constructor) object. Also works with arrays. 65 | * 66 | * @deprecated Function name changed, use the `plainToInstance` method instead. 67 | */ 68 | export function plainToClass(cls: ClassConstructor, plain: V[], options?: ClassTransformOptions): T[]; 69 | export function plainToClass(cls: ClassConstructor, plain: V, options?: ClassTransformOptions): T; 70 | export function plainToClass(cls: ClassConstructor, plain: V | V[], options?: ClassTransformOptions): T | T[] { 71 | return classTransformer.plainToInstance(cls, plain as any, options); 72 | } 73 | 74 | /** 75 | * Converts plain (literal) object to class (constructor) object. Also works with arrays. 76 | */ 77 | export function plainToInstance(cls: ClassConstructor, plain: V[], options?: ClassTransformOptions): T[]; 78 | export function plainToInstance(cls: ClassConstructor, plain: V, options?: ClassTransformOptions): T; 79 | export function plainToInstance( 80 | cls: ClassConstructor, 81 | plain: V | V[], 82 | options?: ClassTransformOptions 83 | ): T | T[] { 84 | return classTransformer.plainToInstance(cls, plain as any, options); 85 | } 86 | 87 | /** 88 | * Converts plain (literal) object to class (constructor) object. 89 | * Uses given object as source object (it means fills given object with data from plain object). 90 | * Also works with arrays. 91 | * 92 | * @deprecated This function is being removed. The current implementation is incorrect as it modifies the source object. 93 | */ 94 | export function plainToClassFromExist(clsObject: T[], plain: V[], options?: ClassTransformOptions): T[]; 95 | export function plainToClassFromExist(clsObject: T, plain: V, options?: ClassTransformOptions): T; 96 | export function plainToClassFromExist(clsObject: T, plain: V | V[], options?: ClassTransformOptions): T | T[] { 97 | return classTransformer.plainToClassFromExist(clsObject, plain, options); 98 | } 99 | 100 | /** 101 | * Converts class (constructor) object to new class (constructor) object. Also works with arrays. 102 | */ 103 | export function instanceToInstance(object: T, options?: ClassTransformOptions): T; 104 | export function instanceToInstance(object: T[], options?: ClassTransformOptions): T[]; 105 | export function instanceToInstance(object: T | T[], options?: ClassTransformOptions): T | T[] { 106 | return classTransformer.instanceToInstance(object, options); 107 | } 108 | 109 | /** 110 | * Converts class (constructor) object to plain (literal) object. 111 | * Uses given plain object as source object (it means fills given plain object with data from class object). 112 | * Also works with arrays. 113 | * 114 | * @deprecated This function is being removed. The current implementation is incorrect as it modifies the source object. 115 | */ 116 | export function classToClassFromExist(object: T, fromObject: T, options?: ClassTransformOptions): T; 117 | export function classToClassFromExist(object: T, fromObjects: T[], options?: ClassTransformOptions): T[]; 118 | export function classToClassFromExist(object: T, fromObject: T | T[], options?: ClassTransformOptions): T | T[] { 119 | return classTransformer.classToClassFromExist(object, fromObject, options); 120 | } 121 | 122 | /** 123 | * Serializes given object to a JSON string. 124 | * 125 | * @deprecated This function is being removed. Please use 126 | * ``` 127 | * JSON.stringify(instanceToPlain(object, options)) 128 | * ``` 129 | */ 130 | export function serialize(object: T, options?: ClassTransformOptions): string; 131 | export function serialize(object: T[], options?: ClassTransformOptions): string; 132 | export function serialize(object: T | T[], options?: ClassTransformOptions): string { 133 | return classTransformer.serialize(object, options); 134 | } 135 | 136 | /** 137 | * Deserializes given JSON string to a object of the given class. 138 | * 139 | * @deprecated This function is being removed. Please use the following instead: 140 | * ``` 141 | * instanceToClass(cls, JSON.parse(json), options) 142 | * ``` 143 | */ 144 | export function deserialize(cls: ClassConstructor, json: string, options?: ClassTransformOptions): T { 145 | return classTransformer.deserialize(cls, json, options); 146 | } 147 | 148 | /** 149 | * Deserializes given JSON string to an array of objects of the given class. 150 | * 151 | * @deprecated This function is being removed. Please use the following instead: 152 | * ``` 153 | * JSON.parse(json).map(value => instanceToClass(cls, value, options)) 154 | * ``` 155 | * 156 | */ 157 | export function deserializeArray(cls: ClassConstructor, json: string, options?: ClassTransformOptions): T[] { 158 | return classTransformer.deserializeArray(cls, json, options); 159 | } 160 | -------------------------------------------------------------------------------- /src/interfaces/class-constructor.type.ts: -------------------------------------------------------------------------------- 1 | export type ClassConstructor = { 2 | new (...args: any[]): T; 3 | }; 4 | -------------------------------------------------------------------------------- /src/interfaces/class-transformer-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { TargetMap } from './target-map.interface'; 2 | 3 | /** 4 | * Options to be passed during transformation. 5 | */ 6 | export interface ClassTransformOptions { 7 | /** 8 | * Exclusion strategy. By default exposeAll is used, which means that it will expose all properties are transformed 9 | * by default. 10 | */ 11 | strategy?: 'excludeAll' | 'exposeAll'; 12 | 13 | /** 14 | * Indicates if extraneous properties should be excluded from the value when converting a plain value to a class. 15 | * 16 | * This option requires that each property on the target class has at least one `@Expose` or `@Exclude` decorator 17 | * assigned from this library. 18 | */ 19 | excludeExtraneousValues?: boolean; 20 | 21 | /** 22 | * Only properties with given groups gonna be transformed. 23 | */ 24 | groups?: string[]; 25 | 26 | /** 27 | * Only properties with "since" > version < "until" gonna be transformed. 28 | */ 29 | version?: number; 30 | 31 | /** 32 | * Excludes properties with the given prefixes. For example, if you mark your private properties with "_" and "__" 33 | * you can set this option's value to ["_", "__"] and all private properties will be skipped. 34 | * This works only for "exposeAll" strategy. 35 | */ 36 | excludePrefixes?: string[]; 37 | 38 | /** 39 | * If set to true then class transformer will ignore the effect of all @Expose and @Exclude decorators. 40 | * This option is useful if you want to kinda clone your object but do not apply decorators affects. 41 | * 42 | * __NOTE:__ You may still have to add the decorators to make other options work. 43 | */ 44 | ignoreDecorators?: boolean; 45 | 46 | /** 47 | * Target maps allows to set a Types of the transforming object without using @Type decorator. 48 | * This is useful when you are transforming external classes, or if you already have type metadata for 49 | * objects and you don't want to set it up again. 50 | */ 51 | targetMaps?: TargetMap[]; 52 | 53 | /** 54 | * If set to true then class transformer will perform a circular check. (circular check is turned off by default) 55 | * This option is useful when you know for sure that your types might have a circular dependency. 56 | */ 57 | enableCircularCheck?: boolean; 58 | 59 | /** 60 | * If set to true then class transformer will try to convert properties implicitly to their target type based on their typing information. 61 | * 62 | * DEFAULT: `false` 63 | */ 64 | enableImplicitConversion?: boolean; 65 | 66 | /** 67 | * If set to true then class transformer will take default values for unprovided fields. 68 | * This is useful when you convert a plain object to a class and have an optional field with a default value. 69 | */ 70 | exposeDefaultValues?: boolean; 71 | 72 | /** 73 | * When set to true, fields with `undefined` as value will be included in class to plain transformation. Otherwise 74 | * those fields will be omitted from the result. 75 | * 76 | * DEFAULT: `true` 77 | */ 78 | exposeUnsetFields?: boolean; 79 | } 80 | -------------------------------------------------------------------------------- /src/interfaces/decorator-options/exclude-options.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Possible transformation options for the @Exclude decorator. 3 | */ 4 | export interface ExcludeOptions { 5 | /** 6 | * Exclude this property only when transforming from plain to class instance. 7 | */ 8 | toClassOnly?: boolean; 9 | 10 | /** 11 | * Exclude this property only when transforming from class instance to plain object. 12 | */ 13 | toPlainOnly?: boolean; 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/decorator-options/expose-options.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Possible transformation options for the @Expose decorator. 3 | */ 4 | export interface ExposeOptions { 5 | /** 6 | * Name of property on the target object to expose the value of this property. 7 | */ 8 | name?: string; 9 | 10 | /** 11 | * First version where this property should be exposed. 12 | * 13 | * Example: 14 | * ```ts 15 | * instanceToPlain(payload, { version: 1.0 }); 16 | * ``` 17 | */ 18 | since?: number; 19 | 20 | /** 21 | * Last version where this property should be exposed. 22 | * 23 | * Example: 24 | * ```ts 25 | * instanceToPlain(payload, { version: 1.0 }); 26 | * ``` 27 | */ 28 | until?: number; 29 | 30 | /** 31 | * List of transformation groups this property belongs to. When set, 32 | * the property will be exposed only when transform is called with 33 | * one of the groups specified. 34 | * 35 | * Example: 36 | * ```ts 37 | * instanceToPlain(payload, { groups: ['user'] }); 38 | * ``` 39 | */ 40 | groups?: string[]; 41 | 42 | /** 43 | * Expose this property only when transforming from plain to class instance. 44 | */ 45 | toClassOnly?: boolean; 46 | 47 | /** 48 | * Expose this property only when transforming from class instance to plain object. 49 | */ 50 | toPlainOnly?: boolean; 51 | } 52 | -------------------------------------------------------------------------------- /src/interfaces/decorator-options/transform-options.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Possible transformation options for the @Transform decorator. 3 | */ 4 | export interface TransformOptions { 5 | /** 6 | * First version where this property should be exposed. 7 | * 8 | * Example: 9 | * ```ts 10 | * instanceToPlain(payload, { version: 1.0 }); 11 | * ``` 12 | */ 13 | since?: number; 14 | 15 | /** 16 | * Last version where this property should be exposed. 17 | * 18 | * Example: 19 | * ```ts 20 | * instanceToPlain(payload, { version: 1.0 }); 21 | * ``` 22 | */ 23 | until?: number; 24 | 25 | /** 26 | * List of transformation groups this property belongs to. When set, 27 | * the property will be exposed only when transform is called with 28 | * one of the groups specified. 29 | * 30 | * Example: 31 | * ```ts 32 | * instanceToPlain(payload, { groups: ['user'] }); 33 | * ``` 34 | */ 35 | groups?: string[]; 36 | 37 | /** 38 | * Expose this property only when transforming from plain to class instance. 39 | */ 40 | toClassOnly?: boolean; 41 | 42 | /** 43 | * Expose this property only when transforming from class instance to plain object. 44 | */ 45 | toPlainOnly?: boolean; 46 | } 47 | -------------------------------------------------------------------------------- /src/interfaces/decorator-options/type-discriminator-descriptor.interface.ts: -------------------------------------------------------------------------------- 1 | import { ClassConstructor } from '..'; 2 | 3 | /** 4 | * Discriminator object containing the type information to select a proper type 5 | * during transformation when a discriminator property is provided. 6 | */ 7 | export interface DiscriminatorDescriptor { 8 | /** 9 | * The name of the property which holds the type information in the received object. 10 | */ 11 | property: string; 12 | /** 13 | * List of the available types. The transformer will try to lookup the object 14 | * with the same key as the value received in the defined discriminator property 15 | * and create an instance of the defined class. 16 | */ 17 | subTypes: { 18 | /** 19 | * Name of the type. 20 | */ 21 | name: string | number; 22 | 23 | /** 24 | * A class constructor which can be used to create the object. 25 | */ 26 | value: ClassConstructor; 27 | }[]; 28 | } 29 | -------------------------------------------------------------------------------- /src/interfaces/decorator-options/type-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { DiscriminatorDescriptor } from './type-discriminator-descriptor.interface'; 2 | 3 | /** 4 | * Possible transformation options for the @Type decorator. 5 | */ 6 | export interface TypeOptions { 7 | /** 8 | * Optional discriminator object, when provided the property value will be 9 | * initialized according to the specified object. 10 | */ 11 | discriminator?: DiscriminatorDescriptor; 12 | 13 | /** 14 | * Indicates whether to keep the discriminator property on the 15 | * transformed object or not. Disabled by default. 16 | * 17 | * @default false 18 | */ 19 | keepDiscriminatorProperty?: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './decorator-options/expose-options.interface'; 2 | export * from './decorator-options/exclude-options.interface'; 3 | export * from './decorator-options/transform-options.interface'; 4 | export * from './decorator-options/type-discriminator-descriptor.interface'; 5 | export * from './decorator-options/type-options.interface'; 6 | export * from './metadata/exclude-metadata.interface'; 7 | export * from './metadata/expose-metadata.interface'; 8 | export * from './metadata/transform-metadata.interface'; 9 | export * from './metadata/transform-fn-params.interface'; 10 | export * from './metadata/type-metadata.interface'; 11 | export * from './class-constructor.type'; 12 | export * from './class-transformer-options.interface'; 13 | export * from './target-map.interface'; 14 | export * from './type-help-options.interface'; 15 | -------------------------------------------------------------------------------- /src/interfaces/metadata/exclude-metadata.interface.ts: -------------------------------------------------------------------------------- 1 | import { ExcludeOptions } from '..'; 2 | 3 | /** 4 | * This object represents metadata assigned to a property via the @Exclude decorator. 5 | */ 6 | export interface ExcludeMetadata { 7 | target: Function; 8 | 9 | /** 10 | * The property name this metadata belongs to on the target (class or property). 11 | * 12 | * Note: If the decorator is applied to a class the propertyName will be undefined. 13 | */ 14 | propertyName: string | undefined; 15 | 16 | /** 17 | * Options passed to the @Exclude operator for this property. 18 | */ 19 | options: ExcludeOptions; 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/metadata/expose-metadata.interface.ts: -------------------------------------------------------------------------------- 1 | import { ExposeOptions } from '..'; 2 | 3 | /** 4 | * This object represents metadata assigned to a property via the @Expose decorator. 5 | */ 6 | export interface ExposeMetadata { 7 | target: Function; 8 | 9 | /** 10 | * The property name this metadata belongs to on the target (class or property). 11 | * 12 | * Note: If the decorator is applied to a class the propertyName will be undefined. 13 | */ 14 | propertyName: string | undefined; 15 | 16 | /** 17 | * Options passed to the @Expose operator for this property. 18 | */ 19 | options: ExposeOptions; 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/metadata/transform-fn-params.interface.ts: -------------------------------------------------------------------------------- 1 | import { TransformationType } from '../../enums'; 2 | import { ClassTransformOptions } from '../class-transformer-options.interface'; 3 | 4 | export interface TransformFnParams { 5 | value: any; 6 | key: string; 7 | obj: any; 8 | type: TransformationType; 9 | options: ClassTransformOptions; 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/metadata/transform-metadata.interface.ts: -------------------------------------------------------------------------------- 1 | import { TransformOptions } from '..'; 2 | import { TransformFnParams } from './transform-fn-params.interface'; 3 | 4 | /** 5 | * This object represents metadata assigned to a property via the @Transform decorator. 6 | */ 7 | export interface TransformMetadata { 8 | target: Function; 9 | 10 | /** 11 | * The property name this metadata belongs to on the target (property only). 12 | */ 13 | propertyName: string; 14 | 15 | /** 16 | * The custom transformation function provided by the user in the @Transform decorator. 17 | */ 18 | transformFn: (params: TransformFnParams) => any; 19 | 20 | /** 21 | * Options passed to the @Transform operator for this property. 22 | */ 23 | options: TransformOptions; 24 | } 25 | -------------------------------------------------------------------------------- /src/interfaces/metadata/type-metadata.interface.ts: -------------------------------------------------------------------------------- 1 | import { TypeHelpOptions, TypeOptions } from '..'; 2 | 3 | /** 4 | * This object represents metadata assigned to a property via the @Type decorator. 5 | */ 6 | export interface TypeMetadata { 7 | target: Function; 8 | 9 | /** 10 | * The property name this metadata belongs to on the target (property only). 11 | */ 12 | propertyName: string; 13 | 14 | /** 15 | * The type guessed from assigned Reflect metadata ('design:type') 16 | */ 17 | reflectedType: any; 18 | 19 | /** 20 | * The custom function provided by the user in the @Type decorator which 21 | * returns the target type for the transformation. 22 | */ 23 | typeFunction: (options?: TypeHelpOptions) => Function; 24 | 25 | /** 26 | * Options passed to the @Type operator for this property. 27 | */ 28 | options: TypeOptions; 29 | } 30 | -------------------------------------------------------------------------------- /src/interfaces/target-map.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows to specify a map of Types in the object without using @Type decorator. 3 | * This is useful when you have external classes. 4 | */ 5 | export interface TargetMap { 6 | /** 7 | * Target which Types are being specified. 8 | */ 9 | target: Function; 10 | 11 | /** 12 | * List of properties and their Types. 13 | */ 14 | properties: { [key: string]: Function }; 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/type-help-options.interface.ts: -------------------------------------------------------------------------------- 1 | // TODO: Document this interface. What does each property means? 2 | export interface TypeHelpOptions { 3 | newObject: any; 4 | object: Record; 5 | property: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/storage.ts: -------------------------------------------------------------------------------- 1 | import { MetadataStorage } from './MetadataStorage'; 2 | import { getGlobal } from './utils'; 3 | 4 | const globalScope = getGlobal(); 5 | 6 | /** 7 | * Default metadata storage is used as singleton and can be used to storage all metadatas. 8 | */ 9 | if (!globalScope.classTransformerMetadataStorage) { 10 | globalScope.classTransformerMetadataStorage = new MetadataStorage(); 11 | } 12 | 13 | export const defaultMetadataStorage = globalScope.classTransformerMetadataStorage; 14 | -------------------------------------------------------------------------------- /src/utils/get-global.util.spect.ts: -------------------------------------------------------------------------------- 1 | import { getGlobal } from '.'; 2 | 3 | describe('getGlobal()', () => { 4 | it('should return true if Buffer is present in globalThis', () => { 5 | expect(getGlobal().Buffer).toBe(true); 6 | }); 7 | 8 | it('should return false if Buffer is not present in globalThis', () => { 9 | const bufferImp = global.Buffer; 10 | delete global.Buffer; 11 | 12 | expect(getGlobal().Buffer).toBe(false); 13 | 14 | global.Buffer = bufferImp; 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/utils/get-global.util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function returns the global object across Node and browsers. 3 | * 4 | * Note: `globalThis` is the standardized approach however it has been added to 5 | * Node.js in version 12. We need to include this snippet until Node 12 EOL. 6 | */ 7 | export function getGlobal() { 8 | if (typeof globalThis !== 'undefined') { 9 | return globalThis; 10 | } 11 | 12 | if (typeof global !== 'undefined') { 13 | return global; 14 | } 15 | 16 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 17 | // @ts-ignore: Cannot find name 'window'. 18 | if (typeof window !== 'undefined') { 19 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 20 | // @ts-ignore: Cannot find name 'window'. 21 | return window; 22 | } 23 | 24 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 25 | // @ts-ignore: Cannot find name 'self'. 26 | if (typeof self !== 'undefined') { 27 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 28 | // @ts-ignore: Cannot find name 'self'. 29 | return self; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-global.util'; 2 | export * from './is-promise.util'; 3 | -------------------------------------------------------------------------------- /src/utils/is-promise.util.ts: -------------------------------------------------------------------------------- 1 | export function isPromise(p: any): p is Promise { 2 | return p !== null && typeof p === 'object' && typeof p.then === 'function'; 3 | } 4 | -------------------------------------------------------------------------------- /test/functional/circular-reference-problem.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { instanceToInstance, instanceToPlain, plainToInstance } from '../../src/index'; 3 | import { defaultMetadataStorage } from '../../src/storage'; 4 | import { TransformOperationExecutor } from '../../src/TransformOperationExecutor'; 5 | 6 | describe('circular reference problem', () => { 7 | it('should skip circular reference objects in instanceToPlain operation', () => { 8 | defaultMetadataStorage.clear(); 9 | 10 | class Caption { 11 | text: string; 12 | } 13 | 14 | class Photo { 15 | id: number; 16 | filename: string; 17 | user: User; 18 | users: User[]; 19 | caption: Caption; 20 | } 21 | 22 | class User { 23 | id: number; 24 | firstName: string; 25 | caption: Caption; 26 | photos: Photo[]; 27 | } 28 | 29 | const photo1 = new Photo(); 30 | photo1.id = 1; 31 | photo1.filename = 'me.jpg'; 32 | 33 | const photo2 = new Photo(); 34 | photo2.id = 2; 35 | photo2.filename = 'she.jpg'; 36 | 37 | const caption = new Caption(); 38 | caption.text = 'cool photo'; 39 | 40 | const user = new User(); 41 | user.caption = caption; 42 | user.firstName = 'Umed Khudoiberdiev'; 43 | user.photos = [photo1, photo2]; 44 | 45 | photo1.user = user; 46 | photo2.user = user; 47 | photo1.users = [user]; 48 | photo2.users = [user]; 49 | 50 | photo1.caption = caption; 51 | photo2.caption = caption; 52 | 53 | const plainUser = instanceToPlain(user, { enableCircularCheck: true }); 54 | expect(plainUser).toEqual({ 55 | firstName: 'Umed Khudoiberdiev', 56 | caption: { text: 'cool photo' }, 57 | photos: [ 58 | { 59 | id: 1, 60 | filename: 'me.jpg', 61 | users: [], 62 | caption: { text: 'cool photo' }, 63 | }, 64 | { 65 | id: 2, 66 | filename: 'she.jpg', 67 | users: [], 68 | caption: { text: 'cool photo' }, 69 | }, 70 | ], 71 | }); 72 | }); 73 | 74 | it('should not skip circular reference objects, but handle it correctly in instanceToInstance operation', () => { 75 | defaultMetadataStorage.clear(); 76 | 77 | class Photo { 78 | id: number; 79 | filename: string; 80 | user: User; 81 | users: User[]; 82 | } 83 | 84 | class User { 85 | id: number; 86 | firstName: string; 87 | photos: Photo[]; 88 | } 89 | 90 | const photo1 = new Photo(); 91 | photo1.id = 1; 92 | photo1.filename = 'me.jpg'; 93 | 94 | const photo2 = new Photo(); 95 | photo2.id = 2; 96 | photo2.filename = 'she.jpg'; 97 | 98 | const user = new User(); 99 | user.firstName = 'Umed Khudoiberdiev'; 100 | user.photos = [photo1, photo2]; 101 | 102 | photo1.user = user; 103 | photo2.user = user; 104 | photo1.users = [user]; 105 | photo2.users = [user]; 106 | 107 | const classUser = instanceToInstance(user, { enableCircularCheck: true }); 108 | expect(classUser).not.toBe(user); 109 | expect(classUser).toBeInstanceOf(User); 110 | expect(classUser).toEqual(user); 111 | }); 112 | 113 | describe('enableCircularCheck option', () => { 114 | class Photo { 115 | id: number; 116 | filename: string; 117 | } 118 | 119 | class User { 120 | id: number; 121 | firstName: string; 122 | photos: Photo[]; 123 | } 124 | let isCircularSpy: jest.SpyInstance; 125 | const photo1 = new Photo(); 126 | photo1.id = 1; 127 | photo1.filename = 'me.jpg'; 128 | 129 | const user = new User(); 130 | user.firstName = 'Umed Khudoiberdiev'; 131 | user.photos = [photo1]; 132 | 133 | beforeEach(() => { 134 | isCircularSpy = jest.spyOn(TransformOperationExecutor.prototype, 'isCircular' as any); 135 | }); 136 | 137 | afterEach(() => { 138 | isCircularSpy.mockRestore(); 139 | }); 140 | 141 | it('enableCircularCheck option is undefined (default)', () => { 142 | plainToInstance>(User, user); 143 | expect(isCircularSpy).not.toHaveBeenCalled(); 144 | }); 145 | 146 | it('enableCircularCheck option is true', () => { 147 | plainToInstance>(User, user, { enableCircularCheck: true }); 148 | expect(isCircularSpy).toHaveBeenCalled(); 149 | }); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /test/functional/custom-transform.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | import 'reflect-metadata'; 3 | import { 4 | instanceToInstance, 5 | instanceToPlain, 6 | ClassTransformOptions, 7 | plainToInstance, 8 | TransformFnParams, 9 | } from '../../src/index'; 10 | import { defaultMetadataStorage } from '../../src/storage'; 11 | import { Expose, Transform, Type } from '../../src/decorators'; 12 | import { TransformationType } from '../../src/enums'; 13 | 14 | describe('custom transformation decorator', () => { 15 | it('@Expose decorator with "name" option should work with @Transform decorator', () => { 16 | defaultMetadataStorage.clear(); 17 | 18 | class User { 19 | @Expose({ name: 'user_name' }) 20 | @Transform(({ value }) => value.toUpperCase()) 21 | name: string; 22 | } 23 | 24 | const plainUser = { 25 | user_name: 'Johny Cage', 26 | }; 27 | 28 | const classedUser = plainToInstance(User, plainUser); 29 | expect(classedUser.name).toEqual('JOHNY CAGE'); 30 | }); 31 | 32 | it('@Transform decorator logic should be executed depend of toPlainOnly and toClassOnly set', () => { 33 | defaultMetadataStorage.clear(); 34 | 35 | class User { 36 | id: number; 37 | name: string; 38 | 39 | @Transform(({ value }) => value.toString(), { toPlainOnly: true }) 40 | @Transform(({ value }) => 'custom-transformed', { toClassOnly: true }) 41 | date: Date; 42 | } 43 | 44 | const plainUser = { 45 | id: 1, 46 | name: 'Johny Cage', 47 | date: new Date().valueOf(), 48 | }; 49 | 50 | const user = new User(); 51 | user.id = 1; 52 | user.name = 'Johny Cage'; 53 | user.date = new Date(); 54 | 55 | const classedUser = plainToInstance(User, plainUser); 56 | expect(classedUser).toBeInstanceOf(User); 57 | expect(classedUser.id).toEqual(1); 58 | expect(classedUser.name).toEqual('Johny Cage'); 59 | expect(classedUser.date).toBe('custom-transformed'); 60 | 61 | const plainedUser = instanceToPlain(user); 62 | expect(plainedUser).not.toBeInstanceOf(User); 63 | expect(plainedUser).toEqual({ 64 | id: 1, 65 | name: 'Johny Cage', 66 | date: user.date.toString(), 67 | }); 68 | }); 69 | 70 | it('versions and groups should work with @Transform decorator too', () => { 71 | defaultMetadataStorage.clear(); 72 | 73 | class User { 74 | id: number; 75 | name: string; 76 | 77 | @Type(() => Date) 78 | @Transform(({ value }) => 'custom-transformed-version-check', { since: 1, until: 2 }) 79 | date: Date; 80 | 81 | @Type(() => Date) 82 | @Transform(({ value }) => value.toString(), { groups: ['user'] }) 83 | lastVisitDate: Date; 84 | } 85 | 86 | const plainUser = { 87 | id: 1, 88 | name: 'Johny Cage', 89 | date: new Date().valueOf(), 90 | lastVisitDate: new Date().valueOf(), 91 | }; 92 | 93 | const classedUser1 = plainToInstance(User, plainUser); 94 | expect(classedUser1).toBeInstanceOf(User); 95 | expect(classedUser1.id).toEqual(1); 96 | expect(classedUser1.name).toEqual('Johny Cage'); 97 | expect(classedUser1.date).toBe('custom-transformed-version-check'); 98 | 99 | const classedUser2 = plainToInstance(User, plainUser, { version: 0.5 }); 100 | expect(classedUser2).toBeInstanceOf(User); 101 | expect(classedUser2.id).toEqual(1); 102 | expect(classedUser2.name).toEqual('Johny Cage'); 103 | expect(classedUser2.date).toBeInstanceOf(Date); 104 | 105 | const classedUser3 = plainToInstance(User, plainUser, { version: 1 }); 106 | expect(classedUser3).toBeInstanceOf(User); 107 | expect(classedUser3.id).toEqual(1); 108 | expect(classedUser3.name).toEqual('Johny Cage'); 109 | expect(classedUser3.date).toBe('custom-transformed-version-check'); 110 | 111 | const classedUser4 = plainToInstance(User, plainUser, { version: 2 }); 112 | expect(classedUser4).toBeInstanceOf(User); 113 | expect(classedUser4.id).toEqual(1); 114 | expect(classedUser4.name).toEqual('Johny Cage'); 115 | expect(classedUser4.date).toBeInstanceOf(Date); 116 | 117 | const classedUser5 = plainToInstance(User, plainUser, { groups: ['user'] }); 118 | expect(classedUser5).toBeInstanceOf(User); 119 | expect(classedUser5.id).toEqual(1); 120 | expect(classedUser5.name).toEqual('Johny Cage'); 121 | expect(classedUser5.lastVisitDate).toEqual(new Date(plainUser.lastVisitDate).toString()); 122 | }); 123 | 124 | it('@Transform decorator callback should be given correct arguments', () => { 125 | defaultMetadataStorage.clear(); 126 | 127 | let keyArg: string; 128 | let objArg: any; 129 | let typeArg: TransformationType; 130 | let optionsArg: ClassTransformOptions; 131 | 132 | function transformCallback({ value, key, obj, type, options }: TransformFnParams): any { 133 | keyArg = key; 134 | objArg = obj; 135 | typeArg = type; 136 | optionsArg = options; 137 | return value; 138 | } 139 | 140 | class User { 141 | @Transform(transformCallback, { toPlainOnly: true }) 142 | @Transform(transformCallback, { toClassOnly: true }) 143 | name: string; 144 | } 145 | 146 | const plainUser = { 147 | name: 'Johny Cage', 148 | }; 149 | const options: ClassTransformOptions = { 150 | groups: ['user', 'user.email'], 151 | version: 2, 152 | }; 153 | 154 | plainToInstance(User, plainUser, options); 155 | expect(keyArg).toBe('name'); 156 | expect(objArg).toEqual(plainUser); 157 | expect(typeArg).toEqual(TransformationType.PLAIN_TO_CLASS); 158 | expect(optionsArg.groups).toBe(options.groups); 159 | expect(optionsArg.version).toBe(options.version); 160 | 161 | const user = new User(); 162 | user.name = 'Johny Cage'; 163 | optionsArg = undefined; 164 | 165 | instanceToPlain(user, options); 166 | expect(keyArg).toBe('name'); 167 | expect(objArg).toEqual(user); 168 | expect(typeArg).toEqual(TransformationType.CLASS_TO_PLAIN); 169 | expect(optionsArg.groups).toBe(options.groups); 170 | expect(optionsArg.version).toBe(options.version); 171 | }); 172 | 173 | let model: any; 174 | it('should serialize json into model instance of class Person', () => { 175 | defaultMetadataStorage.clear(); 176 | expect(() => { 177 | const json = { 178 | name: 'John Doe', 179 | address: { 180 | street: 'Main Street 25', 181 | tel: '5454-534-645', 182 | zip: 10353, 183 | country: 'West Samoa', 184 | }, 185 | age: 25, 186 | hobbies: [ 187 | { type: 'sport', name: 'sailing' }, 188 | { type: 'relax', name: 'reading' }, 189 | { type: 'sport', name: 'jogging' }, 190 | { type: 'relax', name: 'movies' }, 191 | ], 192 | }; 193 | class Hobby { 194 | public type: string; 195 | public name: string; 196 | } 197 | class Address { 198 | public street: string; 199 | 200 | @Expose({ name: 'tel' }) 201 | public telephone: string; 202 | 203 | public zip: number; 204 | 205 | public country: string; 206 | } 207 | class Person { 208 | public name: string; 209 | 210 | @Type(() => Address) 211 | public address: Address; 212 | 213 | @Type(() => Hobby) 214 | @Transform(({ value }) => value.filter((hobby: any) => hobby.type === 'sport'), { toClassOnly: true }) 215 | public hobbies: Hobby[]; 216 | 217 | public age: number; 218 | } 219 | model = plainToInstance(Person, json); 220 | expect(model instanceof Person); 221 | expect(model.address instanceof Address); 222 | model.hobbies.forEach((hobby: Hobby) => expect(hobby instanceof Hobby && hobby.type === 'sport')); 223 | }).not.toThrow(); 224 | }); 225 | 226 | it('should serialize json into model instance of class Person with different possibilities for type of one property (polymorphism)', () => { 227 | defaultMetadataStorage.clear(); 228 | expect(() => { 229 | const json = { 230 | name: 'John Doe', 231 | hobby: { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, 232 | }; 233 | 234 | abstract class Hobby { 235 | public name: string; 236 | } 237 | 238 | class Sports extends Hobby { 239 | // Empty 240 | } 241 | 242 | class Relaxing extends Hobby { 243 | // Empty 244 | } 245 | 246 | class Programming extends Hobby { 247 | @Transform(({ value }) => value.toUpperCase()) 248 | specialAbility: string; 249 | } 250 | 251 | class Person { 252 | public name: string; 253 | 254 | @Type(() => Hobby, { 255 | discriminator: { 256 | property: '__type', 257 | subTypes: [ 258 | { value: Sports, name: 'sports' }, 259 | { value: Relaxing, name: 'relax' }, 260 | { value: Programming, name: 'program' }, 261 | ], 262 | }, 263 | }) 264 | public hobby: any; 265 | } 266 | 267 | const expectedHobby = { name: 'typescript coding', specialAbility: 'TESTING' }; 268 | 269 | const model: Person = plainToInstance(Person, json); 270 | expect(model).toBeInstanceOf(Person); 271 | expect(model.hobby).toBeInstanceOf(Programming); 272 | expect(model.hobby).not.toHaveProperty('__type'); 273 | expect(model.hobby).toHaveProperty('specialAbility', 'TESTING'); 274 | }).not.toThrow(); 275 | }); 276 | 277 | it('should serialize json into model instance of class Person with different types in array (polymorphism)', () => { 278 | defaultMetadataStorage.clear(); 279 | expect(() => { 280 | const json = { 281 | name: 'John Doe', 282 | hobbies: [ 283 | { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, 284 | { __type: 'relax', name: 'sun' }, 285 | ], 286 | }; 287 | 288 | abstract class Hobby { 289 | public name: string; 290 | } 291 | 292 | class Sports extends Hobby { 293 | // Empty 294 | } 295 | 296 | class Relaxing extends Hobby { 297 | // Empty 298 | } 299 | 300 | class Programming extends Hobby { 301 | @Transform(({ value }) => value.toUpperCase()) 302 | specialAbility: string; 303 | } 304 | 305 | class Person { 306 | public name: string; 307 | 308 | @Type(() => Hobby, { 309 | discriminator: { 310 | property: '__type', 311 | subTypes: [ 312 | { value: Sports, name: 'sports' }, 313 | { value: Relaxing, name: 'relax' }, 314 | { value: Programming, name: 'program' }, 315 | ], 316 | }, 317 | }) 318 | public hobbies: any[]; 319 | } 320 | 321 | const model: Person = plainToInstance(Person, json); 322 | expect(model).toBeInstanceOf(Person); 323 | expect(model.hobbies[0]).toBeInstanceOf(Programming); 324 | expect(model.hobbies[1]).toBeInstanceOf(Relaxing); 325 | expect(model.hobbies[0]).not.toHaveProperty('__type'); 326 | expect(model.hobbies[1]).not.toHaveProperty('__type'); 327 | expect(model.hobbies[1]).toHaveProperty('name', 'sun'); 328 | expect(model.hobbies[0]).toHaveProperty('specialAbility', 'TESTING'); 329 | }).not.toThrow(); 330 | }); 331 | 332 | it('should serialize json into model instance of class Person with different possibilities for type of one property AND keeps discriminator property (polymorphism)', () => { 333 | defaultMetadataStorage.clear(); 334 | expect(() => { 335 | const json = { 336 | name: 'John Doe', 337 | hobby: { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, 338 | }; 339 | 340 | abstract class Hobby { 341 | public name: string; 342 | } 343 | 344 | class Sports extends Hobby { 345 | // Empty 346 | } 347 | 348 | class Relaxing extends Hobby { 349 | // Empty 350 | } 351 | 352 | class Programming extends Hobby { 353 | @Transform(({ value }) => value.toUpperCase()) 354 | specialAbility: string; 355 | } 356 | 357 | class Person { 358 | public name: string; 359 | 360 | @Type(() => Hobby, { 361 | discriminator: { 362 | property: '__type', 363 | subTypes: [ 364 | { value: Sports, name: 'sports' }, 365 | { value: Relaxing, name: 'relax' }, 366 | { value: Programming, name: 'program' }, 367 | ], 368 | }, 369 | keepDiscriminatorProperty: true, 370 | }) 371 | public hobby: any; 372 | } 373 | 374 | const model: Person = plainToInstance(Person, json); 375 | expect(model).toBeInstanceOf(Person); 376 | expect(model.hobby).toBeInstanceOf(Programming); 377 | expect(model.hobby).toHaveProperty('__type'); 378 | expect(model.hobby).toHaveProperty('specialAbility', 'TESTING'); 379 | }).not.toThrow(); 380 | }); 381 | 382 | it('should serialize json into model instance of class Person with different types in array AND keeps discriminator property (polymorphism)', () => { 383 | defaultMetadataStorage.clear(); 384 | expect(() => { 385 | const json = { 386 | name: 'John Doe', 387 | hobbies: [ 388 | { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, 389 | { __type: 'relax', name: 'sun' }, 390 | ], 391 | }; 392 | 393 | abstract class Hobby { 394 | public name: string; 395 | } 396 | 397 | class Sports extends Hobby { 398 | // Empty 399 | } 400 | 401 | class Relaxing extends Hobby { 402 | // Empty 403 | } 404 | 405 | class Programming extends Hobby { 406 | @Transform(({ value }) => value.toUpperCase()) 407 | specialAbility: string; 408 | } 409 | 410 | class Person { 411 | public name: string; 412 | 413 | @Type(() => Hobby, { 414 | discriminator: { 415 | property: '__type', 416 | subTypes: [ 417 | { value: Sports, name: 'sports' }, 418 | { value: Relaxing, name: 'relax' }, 419 | { value: Programming, name: 'program' }, 420 | ], 421 | }, 422 | keepDiscriminatorProperty: true, 423 | }) 424 | public hobbies: any[]; 425 | } 426 | 427 | const model: Person = plainToInstance(Person, json); 428 | expect(model).toBeInstanceOf(Person); 429 | expect(model.hobbies[0]).toBeInstanceOf(Programming); 430 | expect(model.hobbies[1]).toBeInstanceOf(Relaxing); 431 | expect(model.hobbies[0]).toHaveProperty('__type'); 432 | expect(model.hobbies[1]).toHaveProperty('__type'); 433 | expect(model.hobbies[1]).toHaveProperty('name', 'sun'); 434 | expect(model.hobbies[0]).toHaveProperty('specialAbility', 'TESTING'); 435 | }).not.toThrow(); 436 | }); 437 | 438 | it('should deserialize class Person into json with different possibilities for type of one property (polymorphism)', () => { 439 | defaultMetadataStorage.clear(); 440 | expect(() => { 441 | abstract class Hobby { 442 | public name: string; 443 | } 444 | 445 | class Sports extends Hobby { 446 | // Empty 447 | } 448 | 449 | class Relaxing extends Hobby { 450 | // Empty 451 | } 452 | 453 | class Programming extends Hobby { 454 | @Transform(({ value }) => value.toUpperCase()) 455 | specialAbility: string; 456 | } 457 | 458 | class Person { 459 | public name: string; 460 | 461 | @Type(() => Hobby, { 462 | discriminator: { 463 | property: '__type', 464 | subTypes: [ 465 | { value: Sports, name: 'sports' }, 466 | { value: Relaxing, name: 'relax' }, 467 | { value: Programming, name: 'program' }, 468 | ], 469 | }, 470 | }) 471 | public hobby: any; 472 | } 473 | 474 | const model: Person = new Person(); 475 | const program = new Programming(); 476 | program.name = 'typescript coding'; 477 | program.specialAbility = 'testing'; 478 | model.name = 'John Doe'; 479 | model.hobby = program; 480 | const json: any = instanceToPlain(model); 481 | expect(json).not.toBeInstanceOf(Person); 482 | expect(json.hobby).toHaveProperty('__type', 'program'); 483 | }).not.toThrow(); 484 | }); 485 | 486 | it('should deserialize class Person into json with different types in array (polymorphism)', () => { 487 | defaultMetadataStorage.clear(); 488 | expect(() => { 489 | abstract class Hobby { 490 | public name: string; 491 | } 492 | 493 | class Sports extends Hobby { 494 | // Empty 495 | } 496 | 497 | class Relaxing extends Hobby { 498 | // Empty 499 | } 500 | 501 | class Programming extends Hobby { 502 | @Transform(({ value }) => value.toUpperCase()) 503 | specialAbility: string; 504 | } 505 | 506 | class Person { 507 | public name: string; 508 | 509 | @Type(() => Hobby, { 510 | discriminator: { 511 | property: '__type', 512 | subTypes: [ 513 | { value: Sports, name: 'sports' }, 514 | { value: Relaxing, name: 'relax' }, 515 | { value: Programming, name: 'program' }, 516 | ], 517 | }, 518 | }) 519 | public hobbies: any[]; 520 | } 521 | 522 | const model: Person = new Person(); 523 | const sport = new Sports(); 524 | sport.name = 'Football'; 525 | const program = new Programming(); 526 | program.name = 'typescript coding'; 527 | program.specialAbility = 'testing'; 528 | model.name = 'John Doe'; 529 | model.hobbies = [sport, program]; 530 | const json: any = instanceToPlain(model); 531 | expect(json).not.toBeInstanceOf(Person); 532 | expect(json.hobbies[0]).toHaveProperty('__type', 'sports'); 533 | expect(json.hobbies[1]).toHaveProperty('__type', 'program'); 534 | }).not.toThrow(); 535 | }); 536 | 537 | /** 538 | * test-case for issue #520 539 | */ 540 | it('should deserialize undefined union type to undefined', () => { 541 | defaultMetadataStorage.clear(); 542 | expect(() => { 543 | abstract class Hobby { 544 | public name: string; 545 | } 546 | 547 | class Sports extends Hobby { 548 | // Empty 549 | } 550 | 551 | class Relaxing extends Hobby { 552 | // Empty 553 | } 554 | 555 | class Programming extends Hobby { 556 | @Transform(({ value }) => value.toUpperCase()) 557 | specialAbility: string; 558 | } 559 | 560 | class Person { 561 | public name: string; 562 | 563 | @Type(() => Hobby, { 564 | discriminator: { 565 | property: '__type', 566 | subTypes: [ 567 | { value: Sports, name: 'sports' }, 568 | { value: Relaxing, name: 'relax' }, 569 | { value: Programming, name: 'program' }, 570 | ], 571 | }, 572 | }) 573 | public hobby: Hobby; 574 | } 575 | 576 | const model: Person = new Person(); 577 | const sport = new Sports(); 578 | sport.name = 'Football'; 579 | const program = new Programming(); 580 | program.name = 'typescript coding'; 581 | program.specialAbility = 'testing'; 582 | model.name = 'John Doe'; 583 | // NOTE: hobby remains undefined 584 | model.hobby = undefined; 585 | const json: any = instanceToPlain(model); 586 | expect(json).not.toBeInstanceOf(Person); 587 | expect(json.hobby).toBeUndefined(); 588 | }).not.toThrow(); 589 | }); 590 | 591 | it('should transform class Person into class OtherPerson with different possibilities for type of one property (polymorphism)', () => { 592 | defaultMetadataStorage.clear(); 593 | expect(() => { 594 | abstract class Hobby { 595 | public name: string; 596 | } 597 | 598 | class Sports extends Hobby { 599 | // Empty 600 | } 601 | 602 | class Relaxing extends Hobby { 603 | // Empty 604 | } 605 | 606 | class Programming extends Hobby { 607 | @Transform(({ value }) => value.toUpperCase()) 608 | specialAbility: string; 609 | } 610 | 611 | class Person { 612 | public name: string; 613 | 614 | @Type(() => Hobby, { 615 | discriminator: { 616 | property: '__type', 617 | subTypes: [ 618 | { value: Sports, name: 'sports' }, 619 | { value: Relaxing, name: 'relax' }, 620 | { value: Programming, name: 'program' }, 621 | ], 622 | }, 623 | }) 624 | public hobby: any; 625 | } 626 | 627 | const model: Person = new Person(); 628 | const program = new Programming(); 629 | program.name = 'typescript coding'; 630 | program.specialAbility = 'testing'; 631 | model.name = 'John Doe'; 632 | model.hobby = program; 633 | const person: Person = instanceToInstance(model); 634 | expect(person).toBeInstanceOf(Person); 635 | expect(person.hobby).not.toHaveProperty('__type'); 636 | }).not.toThrow(); 637 | }); 638 | 639 | it('should transform class Person into class OtherPerson with different types in array (polymorphism)', () => { 640 | defaultMetadataStorage.clear(); 641 | expect(() => { 642 | abstract class Hobby { 643 | public name: string; 644 | } 645 | 646 | class Sports extends Hobby { 647 | // Empty 648 | } 649 | 650 | class Relaxing extends Hobby { 651 | // Empty 652 | } 653 | 654 | class Programming extends Hobby { 655 | @Transform(({ value }) => value.toUpperCase()) 656 | specialAbility: string; 657 | } 658 | 659 | class Person { 660 | public name: string; 661 | 662 | @Type(() => Hobby, { 663 | discriminator: { 664 | property: '__type', 665 | subTypes: [ 666 | { value: Sports, name: 'sports' }, 667 | { value: Relaxing, name: 'relax' }, 668 | { value: Programming, name: 'program' }, 669 | ], 670 | }, 671 | }) 672 | public hobbies: any[]; 673 | } 674 | 675 | const model: Person = new Person(); 676 | const sport = new Sports(); 677 | sport.name = 'Football'; 678 | const program = new Programming(); 679 | program.name = 'typescript coding'; 680 | program.specialAbility = 'testing'; 681 | model.name = 'John Doe'; 682 | model.hobbies = [sport, program]; 683 | const person: Person = instanceToInstance(model); 684 | expect(person).toBeInstanceOf(Person); 685 | expect(person.hobbies[0]).not.toHaveProperty('__type'); 686 | expect(person.hobbies[1]).not.toHaveProperty('__type'); 687 | }).not.toThrow(); 688 | }); 689 | 690 | it('should serialize json into model instance of class Person with different possibilities for type of one property AND uses default as fallback (polymorphism)', () => { 691 | defaultMetadataStorage.clear(); 692 | expect(() => { 693 | const json = { 694 | name: 'John Doe', 695 | hobby: { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, 696 | }; 697 | 698 | abstract class Hobby { 699 | public name: string; 700 | } 701 | 702 | class Sports extends Hobby { 703 | // Empty 704 | } 705 | 706 | class Relaxing extends Hobby { 707 | // Empty 708 | } 709 | 710 | class Programming extends Hobby { 711 | @Transform(({ value }) => value.toUpperCase()) 712 | specialAbility: string; 713 | } 714 | 715 | class Person { 716 | public name: string; 717 | 718 | @Type(() => Hobby, { 719 | discriminator: { 720 | property: '__type', 721 | subTypes: [], 722 | }, 723 | }) 724 | public hobby: any; 725 | } 726 | 727 | const model: Person = plainToInstance(Person, json); 728 | expect(model).toBeInstanceOf(Person); 729 | expect(model.hobby).toBeInstanceOf(Hobby); 730 | expect(model.hobby).not.toHaveProperty('__type'); 731 | expect(model.hobby).toHaveProperty('specialAbility', 'testing'); 732 | }).not.toThrow(); 733 | }); 734 | 735 | it('should serialize json into model instance of class Person with different types in array AND uses default as fallback (polymorphism)', () => { 736 | defaultMetadataStorage.clear(); 737 | expect(() => { 738 | const json = { 739 | name: 'John Doe', 740 | hobbies: [ 741 | { __type: 'program', name: 'typescript coding', specialAbility: 'testing' }, 742 | { __type: 'relax', name: 'sun' }, 743 | ], 744 | }; 745 | 746 | abstract class Hobby { 747 | public name: string; 748 | } 749 | 750 | class Sports extends Hobby { 751 | // Empty 752 | } 753 | 754 | class Relaxing extends Hobby { 755 | // Empty 756 | } 757 | 758 | class Programming extends Hobby { 759 | @Transform(({ value }) => value.toUpperCase()) 760 | specialAbility: string; 761 | } 762 | 763 | class Person { 764 | public name: string; 765 | 766 | @Type(() => Hobby, { 767 | discriminator: { 768 | property: '__type', 769 | subTypes: [], 770 | }, 771 | }) 772 | public hobbies: any[]; 773 | } 774 | 775 | const model: Person = plainToInstance(Person, json); 776 | expect(model).toBeInstanceOf(Person); 777 | expect(model.hobbies[0]).toBeInstanceOf(Hobby); 778 | expect(model.hobbies[1]).toBeInstanceOf(Hobby); 779 | expect(model.hobbies[0]).not.toHaveProperty('__type'); 780 | expect(model.hobbies[1]).not.toHaveProperty('__type'); 781 | expect(model.hobbies[1]).toHaveProperty('name', 'sun'); 782 | expect(model.hobbies[0]).toHaveProperty('specialAbility', 'testing'); 783 | }).not.toThrow(); 784 | }); 785 | 786 | it('should serialize a model into json', () => { 787 | expect(() => { 788 | instanceToPlain(model); 789 | }).not.toThrow(); 790 | }); 791 | }); 792 | -------------------------------------------------------------------------------- /test/functional/default-values.spec.ts: -------------------------------------------------------------------------------- 1 | import { Expose, plainToInstance, Transform } from '../../src'; 2 | 3 | describe('expose default values', () => { 4 | class User { 5 | @Expose({ name: 'AGE' }) 6 | @Transform(({ value }) => parseInt(value, 10)) 7 | age: number; 8 | 9 | @Expose({ name: 'AGE_WITH_DEFAULT' }) 10 | @Transform(({ value }) => parseInt(value, 10)) 11 | ageWithDefault?: number = 18; 12 | 13 | @Expose({ name: 'FIRST_NAME' }) 14 | firstName: string; 15 | 16 | @Expose({ name: 'FIRST_NAME_WITH_DEFAULT' }) 17 | firstNameWithDefault?: string = 'default first name'; 18 | 19 | @Transform(({ value }) => !!value) 20 | admin: boolean; 21 | 22 | @Transform(({ value }) => !!value) 23 | adminWithDefault?: boolean = false; 24 | 25 | lastName: string; 26 | 27 | lastNameWithDefault?: string = 'default last name'; 28 | } 29 | 30 | it('should set default value if nothing provided', () => { 31 | const fromPlainUser = {}; 32 | const transformedUser = plainToInstance(User, fromPlainUser, { exposeDefaultValues: true }); 33 | 34 | expect(transformedUser).toBeInstanceOf(User); 35 | expect(transformedUser).toEqual({ 36 | age: undefined, 37 | ageWithDefault: 18, 38 | firstName: undefined, 39 | firstNameWithDefault: 'default first name', 40 | adminWithDefault: false, 41 | lastNameWithDefault: 'default last name', 42 | }); 43 | }); 44 | 45 | it('should take exposed values and ignore defaults', () => { 46 | const fromPlainUser = {}; 47 | const transformedUser = plainToInstance(User, fromPlainUser); 48 | 49 | expect(transformedUser).toBeInstanceOf(User); 50 | expect(transformedUser).toEqual({ 51 | age: NaN, 52 | ageWithDefault: NaN, 53 | firstName: undefined, 54 | firstNameWithDefault: undefined, 55 | adminWithDefault: false, 56 | lastNameWithDefault: 'default last name', 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/functional/es6-data-types.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { instanceToPlain, plainToInstance, Expose } from '../../src/index'; 3 | import { defaultMetadataStorage } from '../../src/storage'; 4 | import { Type } from '../../src/decorators'; 5 | 6 | describe('es6 data types', () => { 7 | it('using Map', () => { 8 | defaultMetadataStorage.clear(); 9 | 10 | class User { 11 | id: number; 12 | name: string; 13 | @Type(() => String) 14 | weapons: Map; 15 | } 16 | 17 | const plainUser = { 18 | id: 1, 19 | name: 'Max Pain', 20 | weapons: { 21 | firstWeapon: 'knife', 22 | secondWeapon: 'eagle', 23 | thirdWeapon: 'ak-47', 24 | }, 25 | }; 26 | 27 | const weapons = new Map(); 28 | weapons.set('firstWeapon', 'knife'); 29 | weapons.set('secondWeapon', 'eagle'); 30 | weapons.set('thirdWeapon', 'ak-47'); 31 | 32 | const user = new User(); 33 | user.id = 1; 34 | user.name = 'Max Pain'; 35 | user.weapons = weapons; 36 | 37 | const classedUser = plainToInstance(User, plainUser); 38 | expect(classedUser).toBeInstanceOf(User); 39 | expect(classedUser.id).toEqual(1); 40 | expect(classedUser.name).toEqual('Max Pain'); 41 | expect(classedUser.weapons).toBeInstanceOf(Map); 42 | expect(classedUser.weapons.size).toEqual(3); 43 | expect(classedUser.weapons.get('firstWeapon')).toEqual('knife'); 44 | expect(classedUser.weapons.get('secondWeapon')).toEqual('eagle'); 45 | expect(classedUser.weapons.get('thirdWeapon')).toEqual('ak-47'); 46 | 47 | const plainedUser = instanceToPlain(user); 48 | expect(plainedUser).not.toBeInstanceOf(User); 49 | expect(plainedUser).toEqual({ 50 | id: 1, 51 | name: 'Max Pain', 52 | weapons: { 53 | firstWeapon: 'knife', 54 | secondWeapon: 'eagle', 55 | thirdWeapon: 'ak-47', 56 | }, 57 | }); 58 | }); 59 | 60 | it('using Set', () => { 61 | defaultMetadataStorage.clear(); 62 | 63 | class User { 64 | id: number; 65 | name: string; 66 | @Type(() => Set) 67 | weapons: Set; 68 | } 69 | 70 | const plainUser = { 71 | id: 1, 72 | name: 'Max Pain', 73 | weapons: ['knife', 'eagle', 'ak-47'], 74 | }; 75 | 76 | const weapons = new Set(); 77 | weapons.add('knife'); 78 | weapons.add('eagle'); 79 | weapons.add('ak-47'); 80 | 81 | const user = new User(); 82 | user.id = 1; 83 | user.name = 'Max Pain'; 84 | user.weapons = weapons; 85 | 86 | const classedUser = plainToInstance(User, plainUser); 87 | expect(classedUser).toBeInstanceOf(User); 88 | expect(classedUser.id).toEqual(1); 89 | expect(classedUser.name).toEqual('Max Pain'); 90 | expect(classedUser.weapons).toBeInstanceOf(Set); 91 | expect(classedUser.weapons.size).toEqual(3); 92 | expect(classedUser.weapons.has('knife')).toBeTruthy(); 93 | expect(classedUser.weapons.has('eagle')).toBeTruthy(); 94 | expect(classedUser.weapons.has('ak-47')).toBeTruthy(); 95 | 96 | const plainedUser = instanceToPlain(user); 97 | expect(plainedUser).not.toBeInstanceOf(User); 98 | expect(plainedUser).toEqual({ 99 | id: 1, 100 | name: 'Max Pain', 101 | weapons: ['knife', 'eagle', 'ak-47'], 102 | }); 103 | }); 104 | 105 | it('using Map with objects', () => { 106 | defaultMetadataStorage.clear(); 107 | 108 | class Weapon { 109 | constructor(public model: string, public range: number) {} 110 | } 111 | 112 | class User { 113 | id: number; 114 | name: string; 115 | @Type(() => Weapon) 116 | weapons: Map; 117 | } 118 | 119 | const plainUser = { 120 | id: 1, 121 | name: 'Max Pain', 122 | weapons: { 123 | firstWeapon: { 124 | model: 'knife', 125 | range: 1, 126 | }, 127 | secondWeapon: { 128 | model: 'eagle', 129 | range: 200, 130 | }, 131 | thirdWeapon: { 132 | model: 'ak-47', 133 | range: 800, 134 | }, 135 | }, 136 | }; 137 | 138 | const weapons = new Map(); 139 | weapons.set('firstWeapon', new Weapon('knife', 1)); 140 | weapons.set('secondWeapon', new Weapon('eagle', 200)); 141 | weapons.set('thirdWeapon', new Weapon('ak-47', 800)); 142 | 143 | const user = new User(); 144 | user.id = 1; 145 | user.name = 'Max Pain'; 146 | user.weapons = weapons; 147 | 148 | const classedUser = plainToInstance(User, plainUser); 149 | expect(classedUser).toBeInstanceOf(User); 150 | expect(classedUser.id).toEqual(1); 151 | expect(classedUser.name).toEqual('Max Pain'); 152 | expect(classedUser.weapons).toBeInstanceOf(Map); 153 | expect(classedUser.weapons.size).toEqual(3); 154 | expect(classedUser.weapons.get('firstWeapon')).toBeInstanceOf(Weapon); 155 | expect(classedUser.weapons.get('firstWeapon')).toEqual({ 156 | model: 'knife', 157 | range: 1, 158 | }); 159 | expect(classedUser.weapons.get('secondWeapon')).toBeInstanceOf(Weapon); 160 | expect(classedUser.weapons.get('secondWeapon')).toEqual({ 161 | model: 'eagle', 162 | range: 200, 163 | }); 164 | expect(classedUser.weapons.get('thirdWeapon')).toBeInstanceOf(Weapon); 165 | expect(classedUser.weapons.get('thirdWeapon')).toEqual({ 166 | model: 'ak-47', 167 | range: 800, 168 | }); 169 | 170 | const plainedUser = instanceToPlain(user); 171 | expect(plainedUser).not.toBeInstanceOf(User); 172 | expect(plainedUser).toEqual({ 173 | id: 1, 174 | name: 'Max Pain', 175 | weapons: { 176 | firstWeapon: { 177 | model: 'knife', 178 | range: 1, 179 | }, 180 | secondWeapon: { 181 | model: 'eagle', 182 | range: 200, 183 | }, 184 | thirdWeapon: { 185 | model: 'ak-47', 186 | range: 800, 187 | }, 188 | }, 189 | }); 190 | }); 191 | 192 | it('using Set with objects', () => { 193 | defaultMetadataStorage.clear(); 194 | 195 | class Weapon { 196 | constructor(public model: string, public range: number) {} 197 | } 198 | 199 | class User { 200 | id: number; 201 | name: string; 202 | @Type(() => Weapon) 203 | weapons: Set; 204 | } 205 | 206 | const plainUser = { 207 | id: 1, 208 | name: 'Max Pain', 209 | weapons: [ 210 | { model: 'knife', range: 1 }, 211 | { model: 'eagle', range: 200 }, 212 | { model: 'ak-47', range: 800 }, 213 | ], 214 | }; 215 | 216 | const weapons = new Set(); 217 | weapons.add(new Weapon('knife', 1)); 218 | weapons.add(new Weapon('eagle', 200)); 219 | weapons.add(new Weapon('ak-47', 800)); 220 | 221 | const user = new User(); 222 | user.id = 1; 223 | user.name = 'Max Pain'; 224 | user.weapons = weapons; 225 | 226 | const classedUser = plainToInstance(User, plainUser); 227 | expect(classedUser).toBeInstanceOf(User); 228 | expect(classedUser.id).toEqual(1); 229 | expect(classedUser.name).toEqual('Max Pain'); 230 | expect(classedUser.weapons).toBeInstanceOf(Set); 231 | expect(classedUser.weapons.size).toEqual(3); 232 | const it = classedUser.weapons.values(); 233 | const first = it.next().value; 234 | const second = it.next().value; 235 | const third = it.next().value; 236 | expect(first).toBeInstanceOf(Weapon); 237 | expect(first).toEqual({ model: 'knife', range: 1 }); 238 | expect(second).toBeInstanceOf(Weapon); 239 | expect(second).toEqual({ model: 'eagle', range: 200 }); 240 | expect(third).toBeInstanceOf(Weapon); 241 | expect(third).toEqual({ model: 'ak-47', range: 800 }); 242 | 243 | const plainedUser = instanceToPlain(user); 244 | expect(plainedUser).not.toBeInstanceOf(User); 245 | expect(plainedUser).toEqual({ 246 | id: 1, 247 | name: 'Max Pain', 248 | weapons: [ 249 | { model: 'knife', range: 1 }, 250 | { model: 'eagle', range: 200 }, 251 | { model: 'ak-47', range: 800 }, 252 | ], 253 | }); 254 | }); 255 | 256 | it('using Map with objects with Expose', () => { 257 | defaultMetadataStorage.clear(); 258 | 259 | class Weapon { 260 | constructor(public model: string, public range: number) {} 261 | } 262 | 263 | class User { 264 | @Expose() id: number; 265 | @Expose() name: string; 266 | @Expose() 267 | @Type(() => Weapon) 268 | weapons: Map; 269 | } 270 | 271 | const plainUser = { 272 | id: 1, 273 | name: 'Max Pain', 274 | weapons: { 275 | firstWeapon: { 276 | model: 'knife', 277 | range: 1, 278 | }, 279 | secondWeapon: { 280 | model: 'eagle', 281 | range: 200, 282 | }, 283 | thirdWeapon: { 284 | model: 'ak-47', 285 | range: 800, 286 | }, 287 | }, 288 | }; 289 | 290 | const weapons = new Map(); 291 | weapons.set('firstWeapon', new Weapon('knife', 1)); 292 | weapons.set('secondWeapon', new Weapon('eagle', 200)); 293 | weapons.set('thirdWeapon', new Weapon('ak-47', 800)); 294 | 295 | const user = new User(); 296 | user.id = 1; 297 | user.name = 'Max Pain'; 298 | user.weapons = weapons; 299 | const plainedUser = instanceToPlain(user); 300 | expect(plainedUser).not.toBeInstanceOf(User); 301 | expect(plainedUser).toEqual({ 302 | id: 1, 303 | name: 'Max Pain', 304 | weapons: { 305 | firstWeapon: { 306 | model: 'knife', 307 | range: 1, 308 | }, 309 | secondWeapon: { 310 | model: 'eagle', 311 | range: 200, 312 | }, 313 | thirdWeapon: { 314 | model: 'ak-47', 315 | range: 800, 316 | }, 317 | }, 318 | }); 319 | 320 | function checkPlainToClassUser(classUser: User) { 321 | expect(classedUser).toBeInstanceOf(User); 322 | expect(classedUser.id).toEqual(1); 323 | expect(classedUser.name).toEqual('Max Pain'); 324 | expect(classedUser.weapons).toBeInstanceOf(Map); 325 | expect(classedUser.weapons.size).toEqual(3); 326 | expect(classedUser.weapons.get('firstWeapon')).toBeInstanceOf(Weapon); 327 | expect(classedUser.weapons.get('firstWeapon')).toEqual({ 328 | model: 'knife', 329 | range: 1, 330 | }); 331 | expect(classedUser.weapons.get('secondWeapon')).toBeInstanceOf(Weapon); 332 | expect(classedUser.weapons.get('secondWeapon')).toEqual({ 333 | model: 'eagle', 334 | range: 200, 335 | }); 336 | expect(classedUser.weapons.get('thirdWeapon')).toBeInstanceOf(Weapon); 337 | expect(classedUser.weapons.get('thirdWeapon')).toEqual({ 338 | model: 'ak-47', 339 | range: 800, 340 | }); 341 | } 342 | 343 | const classedUser = plainToInstance(User, plainUser, { excludeExtraneousValues: false }); 344 | checkPlainToClassUser(classedUser); 345 | 346 | const classedUser2 = plainToInstance(User, plainUser, { excludeExtraneousValues: true }); 347 | checkPlainToClassUser(classedUser2); 348 | }); 349 | }); 350 | -------------------------------------------------------------------------------- /test/functional/ignore-decorators.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { instanceToPlain } from '../../src/index'; 3 | import { defaultMetadataStorage } from '../../src/storage'; 4 | import { Exclude, Expose } from '../../src/decorators'; 5 | 6 | describe('ignoring specific decorators', () => { 7 | it('when ignoreDecorators is set to true it should ignore all decorators', () => { 8 | defaultMetadataStorage.clear(); 9 | 10 | class User { 11 | id: number; 12 | 13 | @Expose({ name: 'lala' }) 14 | firstName: string; 15 | 16 | @Expose({ groups: ['user'] }) 17 | lastName: string; 18 | 19 | @Exclude() 20 | password: string; 21 | } 22 | 23 | const user = new User(); 24 | user.firstName = 'Umed'; 25 | user.lastName = 'Khudoiberdiev'; 26 | user.password = 'imnosuperman'; 27 | 28 | const plainedUser = instanceToPlain(user, { ignoreDecorators: true }); 29 | expect(plainedUser).toEqual({ 30 | firstName: 'Umed', 31 | lastName: 'Khudoiberdiev', 32 | password: 'imnosuperman', 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/functional/implicit-type-declarations.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { plainToInstance } from '../../src/index'; 3 | import { defaultMetadataStorage } from '../../src/storage'; 4 | import { Expose, Type } from '../../src/decorators'; 5 | 6 | describe('implicit type conversion', () => { 7 | it('should run only when enabled', () => { 8 | defaultMetadataStorage.clear(); 9 | 10 | class SimpleExample { 11 | @Expose() 12 | readonly implicitTypeNumber: number; 13 | 14 | @Expose() 15 | readonly implicitTypeString: string; 16 | } 17 | 18 | const result1: SimpleExample = plainToInstance( 19 | SimpleExample, 20 | { 21 | implicitTypeNumber: '100', 22 | implicitTypeString: 133123, 23 | }, 24 | { enableImplicitConversion: true } 25 | ); 26 | 27 | const result2: SimpleExample = plainToInstance( 28 | SimpleExample, 29 | { 30 | implicitTypeNumber: '100', 31 | implicitTypeString: 133123, 32 | }, 33 | { enableImplicitConversion: false } 34 | ); 35 | 36 | expect(result1).toEqual({ implicitTypeNumber: 100, implicitTypeString: '133123' }); 37 | expect(result2).toEqual({ implicitTypeNumber: '100', implicitTypeString: 133123 }); 38 | }); 39 | }); 40 | 41 | describe('implicit and explicity type declarations', () => { 42 | defaultMetadataStorage.clear(); 43 | 44 | class Example { 45 | @Expose() 46 | readonly implicitTypeViaOtherDecorator: Date; 47 | 48 | @Type() 49 | readonly implicitTypeViaEmptyTypeDecorator: number; 50 | 51 | @Type(() => String) 52 | readonly explicitType: string; 53 | } 54 | 55 | const result: Example = plainToInstance( 56 | Example, 57 | { 58 | implicitTypeViaOtherDecorator: '2018-12-24T12:00:00Z', 59 | implicitTypeViaEmptyTypeDecorator: '100', 60 | explicitType: 100, 61 | }, 62 | { enableImplicitConversion: true } 63 | ); 64 | 65 | it('should use implicitly defined design:type to convert value when no @Type decorator is used', () => { 66 | expect(result.implicitTypeViaOtherDecorator).toBeInstanceOf(Date); 67 | expect(result.implicitTypeViaOtherDecorator.getTime()).toEqual(new Date('2018-12-24T12:00:00Z').getTime()); 68 | }); 69 | 70 | it('should use implicitly defined design:type to convert value when empty @Type() decorator is used', () => { 71 | expect(typeof result.implicitTypeViaEmptyTypeDecorator).toBe('number'); 72 | expect(result.implicitTypeViaEmptyTypeDecorator).toEqual(100); 73 | }); 74 | 75 | it('should use explicitly defined type when @Type(() => Construtable) decorator is used', () => { 76 | expect(typeof result.explicitType).toBe('string'); 77 | expect(result.explicitType).toEqual('100'); 78 | }); 79 | }); 80 | 81 | describe('plainToInstance transforms built-in primitive types properly', () => { 82 | defaultMetadataStorage.clear(); 83 | 84 | class Example { 85 | @Type() 86 | date: Date; 87 | 88 | @Type() 89 | string: string; 90 | 91 | @Type() 92 | string2: string; 93 | 94 | @Type() 95 | number: number; 96 | 97 | @Type() 98 | number2: number; 99 | 100 | @Type() 101 | boolean: boolean; 102 | 103 | @Type() 104 | boolean2: boolean; 105 | } 106 | 107 | const result: Example = plainToInstance( 108 | Example, 109 | { 110 | date: '2018-12-24T12:00:00Z', 111 | string: '100', 112 | string2: 100, 113 | number: '100', 114 | number2: 100, 115 | boolean: 1, 116 | boolean2: 0, 117 | }, 118 | { enableImplicitConversion: true } 119 | ); 120 | 121 | it('should recognize and convert to Date', () => { 122 | expect(result.date).toBeInstanceOf(Date); 123 | expect(result.date.getTime()).toEqual(new Date('2018-12-24T12:00:00Z').getTime()); 124 | }); 125 | 126 | it('should recognize and convert to string', () => { 127 | expect(typeof result.string).toBe('string'); 128 | expect(typeof result.string2).toBe('string'); 129 | expect(result.string).toEqual('100'); 130 | expect(result.string2).toEqual('100'); 131 | }); 132 | 133 | it('should recognize and convert to number', () => { 134 | expect(typeof result.number).toBe('number'); 135 | expect(typeof result.number2).toBe('number'); 136 | expect(result.number).toEqual(100); 137 | expect(result.number2).toEqual(100); 138 | }); 139 | 140 | it('should recognize and convert to boolean', () => { 141 | expect(result.boolean).toBeTruthy(); 142 | expect(result.boolean2).toBeFalsy(); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /test/functional/inheritence.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { plainToInstance, Transform, Type } from '../../src/index'; 3 | import { defaultMetadataStorage } from '../../src/storage'; 4 | 5 | describe('inheritence', () => { 6 | it('decorators should work inside a base class', () => { 7 | defaultMetadataStorage.clear(); 8 | 9 | class Contact { 10 | @Transform(({ value }) => value.toUpperCase()) 11 | name: string; 12 | @Type(() => Date) 13 | birthDate: Date; 14 | } 15 | 16 | class User extends Contact { 17 | @Type(() => Number) 18 | id: number; 19 | email: string; 20 | } 21 | 22 | class Student extends User { 23 | @Transform(({ value }) => value.toUpperCase()) 24 | university: string; 25 | } 26 | 27 | const plainStudent = { 28 | name: 'Johny Cage', 29 | university: 'mit', 30 | birthDate: new Date(1967, 2, 1).toDateString(), 31 | id: 100, 32 | email: 'johnny.cage@gmail.com', 33 | }; 34 | 35 | const classedStudent = plainToInstance(Student, plainStudent); 36 | expect(classedStudent.name).toEqual('JOHNY CAGE'); 37 | expect(classedStudent.university).toEqual('MIT'); 38 | expect(classedStudent.birthDate.getTime()).toEqual(new Date(1967, 2, 1).getTime()); 39 | expect(classedStudent.id).toEqual(plainStudent.id); 40 | expect(classedStudent.email).toEqual(plainStudent.email); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/functional/prevent-array-bomb.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { plainToInstance } from '../../src/index'; 3 | import { defaultMetadataStorage } from '../../src/storage'; 4 | 5 | describe('Prevent array bomb when used with other packages', () => { 6 | it('should not convert specially crafted evil JS object to array', () => { 7 | defaultMetadataStorage.clear(); 8 | 9 | class TestClass { 10 | readonly categories!: string[]; 11 | } 12 | 13 | /** 14 | * We use the prototype of values to guess what is the type of the property. This behavior can be used 15 | * to pass a specially crafted array like object what would be transformed into an array. 16 | * 17 | * Because arrays are numerically indexed, specifying a big enough numerical property as key 18 | * would cause other libraries to iterate over each (undefined) element until the specified value is reached. 19 | * This can be used to cause denial-of-service attacks. 20 | * 21 | * An example of such scenario is the following: 22 | * 23 | * ```ts 24 | * class TestClass { 25 | * @IsArray() 26 | * @IsString({ each: true }) 27 | * readonly categories!: string[]; 28 | * } 29 | * ``` 30 | * 31 | * Using the above class definition with class-validator and receiving the following specially crafted payload without 32 | * the correct protection in place: 33 | * 34 | * `{ '9007199254740990': '9007199254740990', __proto__: [] };` 35 | * 36 | * would result in the creation of an array with length of 9007199254740991 (MAX_SAFE_INTEGER) looking like this: 37 | * 38 | * `[ <9007199254740989 empty elements>, 9007199254740990 ]` 39 | * 40 | * Iterating over this array would take significant time and cause the server to become unresponsive. 41 | */ 42 | 43 | const evilObject = { '100000000': '100000000', __proto__: [] }; 44 | const result = plainToInstance(TestClass, { categories: evilObject }); 45 | 46 | expect(Array.isArray(result.categories)).toBe(false); 47 | expect(result.categories).toEqual({ '100000000': '100000000' }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/functional/promise-field.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { defaultMetadataStorage } from '../../src/storage'; 3 | import { plainToInstance, Type, instanceToPlain } from '../../src'; 4 | 5 | describe('promise field', () => { 6 | it('should transform plan to class with promise field', async () => { 7 | defaultMetadataStorage.clear(); 8 | 9 | class PromiseClass { 10 | promise: Promise; 11 | } 12 | 13 | const plain = { 14 | promise: Promise.resolve('hi'), 15 | }; 16 | 17 | const instance = plainToInstance(PromiseClass, plain); 18 | expect(instance.promise).toBeInstanceOf(Promise); 19 | const value = await instance.promise; 20 | expect(value).toBe('hi'); 21 | }); 22 | 23 | it('should transform class with promise field to plain', async () => { 24 | class PromiseClass { 25 | promise: Promise; 26 | 27 | constructor(promise: Promise) { 28 | this.promise = promise; 29 | } 30 | } 31 | 32 | const instance = new PromiseClass(Promise.resolve('hi')); 33 | const plain = instanceToPlain(instance) as any; 34 | expect(plain).toHaveProperty('promise'); 35 | const value = await plain.promise; 36 | expect(value).toBe('hi'); 37 | }); 38 | 39 | it('should clone promise result', async () => { 40 | defaultMetadataStorage.clear(); 41 | 42 | class PromiseClass { 43 | promise: Promise; 44 | } 45 | 46 | const array = ['hi', 'my', 'name']; 47 | const plain = { 48 | promise: Promise.resolve(array), 49 | }; 50 | 51 | const instance = plainToInstance(PromiseClass, plain); 52 | const value = await instance.promise; 53 | expect(value).toEqual(array); 54 | 55 | // modify transformed array to prove it's not referencing original array 56 | value.push('is'); 57 | expect(value).not.toEqual(array); 58 | }); 59 | 60 | it('should support Type decorator', async () => { 61 | class PromiseClass { 62 | @Type(() => InnerClass) 63 | promise: Promise; 64 | } 65 | 66 | class InnerClass { 67 | position: string; 68 | 69 | constructor(position: string) { 70 | this.position = position; 71 | } 72 | } 73 | 74 | const plain = { 75 | promise: Promise.resolve(new InnerClass('developer')), 76 | }; 77 | 78 | const instance = plainToInstance(PromiseClass, plain); 79 | const value = await instance.promise; 80 | expect(value).toBeInstanceOf(InnerClass); 81 | expect(value.position).toBe('developer'); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/functional/serialization-deserialization.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { deserialize, deserializeArray, serialize } from '../../src/index'; 3 | import { defaultMetadataStorage } from '../../src/storage'; 4 | import { Exclude } from '../../src/decorators'; 5 | 6 | describe('serialization and deserialization objects', () => { 7 | it('should perform serialization and deserialization properly', () => { 8 | defaultMetadataStorage.clear(); 9 | 10 | class User { 11 | firstName: string; 12 | lastName: string; 13 | @Exclude() 14 | password: string; 15 | } 16 | 17 | const user = new User(); 18 | user.firstName = 'Umed'; 19 | user.lastName = 'Khudoiberdiev'; 20 | user.password = 'imnosuperman'; 21 | 22 | const user1 = new User(); 23 | user1.firstName = 'Dima'; 24 | user1.lastName = 'Zotov'; 25 | user1.password = 'imnosuperman'; 26 | 27 | const user2 = new User(); 28 | user2.firstName = 'Bakhrom'; 29 | user2.lastName = 'Baubekov'; 30 | user2.password = 'imnosuperman'; 31 | 32 | const users = [user1, user2]; 33 | const plainUser = { 34 | firstName: 'Umed', 35 | lastName: 'Khudoiberdiev', 36 | password: 'imnosuperman', 37 | }; 38 | 39 | const plainUsers = [ 40 | { 41 | firstName: 'Dima', 42 | lastName: 'Zotov', 43 | password: 'imnobatman', 44 | }, 45 | { 46 | firstName: 'Bakhrom', 47 | lastName: 'Baubekov', 48 | password: 'imnosuperman', 49 | }, 50 | ]; 51 | 52 | const plainedUser = serialize(user); 53 | expect(plainedUser).toEqual( 54 | JSON.stringify({ 55 | firstName: 'Umed', 56 | lastName: 'Khudoiberdiev', 57 | }) 58 | ); 59 | 60 | const plainedUsers = serialize(users); 61 | expect(plainedUsers).toEqual( 62 | JSON.stringify([ 63 | { 64 | firstName: 'Dima', 65 | lastName: 'Zotov', 66 | }, 67 | { 68 | firstName: 'Bakhrom', 69 | lastName: 'Baubekov', 70 | }, 71 | ]) 72 | ); 73 | 74 | const classedUser = deserialize(User, JSON.stringify(plainUser)); 75 | expect(classedUser).toBeInstanceOf(User); 76 | expect(classedUser).toEqual({ 77 | firstName: 'Umed', 78 | lastName: 'Khudoiberdiev', 79 | }); 80 | 81 | const classedUsers = deserializeArray(User, JSON.stringify(plainUsers)); 82 | expect(classedUsers[0]).toBeInstanceOf(User); 83 | expect(classedUsers[1]).toBeInstanceOf(User); 84 | 85 | const userLike1 = new User(); 86 | userLike1.firstName = 'Dima'; 87 | userLike1.lastName = 'Zotov'; 88 | 89 | const userLike2 = new User(); 90 | userLike2.firstName = 'Bakhrom'; 91 | userLike2.lastName = 'Baubekov'; 92 | 93 | expect(classedUsers).toEqual([userLike1, userLike2]); 94 | }); 95 | 96 | it('should successfully deserialize object with unknown nested properties ', () => { 97 | defaultMetadataStorage.clear(); 98 | 99 | class TestObject { 100 | prop: string; 101 | } 102 | 103 | const payload = { 104 | prop: 'Hi', 105 | extra: { 106 | anotherProp: "let's see how this works out!", 107 | }, 108 | }; 109 | 110 | const result = deserialize(TestObject, JSON.stringify(payload)); 111 | expect(result).toBeInstanceOf(TestObject); 112 | expect(result.prop).toEqual('Hi'); 113 | // TODO: We should strip, but it's a breaking change 114 | // (result).extra.should.be.undefined; 115 | }); 116 | 117 | it('should not overwrite non writable properties on deserialize', () => { 118 | class TestObject { 119 | get getterOnlyProp(): string { 120 | return 'I cannot write!'; 121 | } 122 | 123 | normalProp: string = 'Hello!'; 124 | } 125 | 126 | const payload = { 127 | getterOnlyProp: 'I CAN write!', 128 | normalProp: 'Goodbye!', 129 | }; 130 | 131 | const result = deserialize(TestObject, JSON.stringify(payload)); 132 | expect(result.getterOnlyProp).toEqual('I cannot write!'); 133 | expect(result.normalProp).toEqual('Goodbye!'); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /test/functional/transformation-option.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { instanceToPlain, plainToInstance } from '../../src/index'; 3 | import { defaultMetadataStorage } from '../../src/storage'; 4 | import { Exclude, Expose } from '../../src/decorators'; 5 | 6 | describe('filtering by transformation option', () => { 7 | it('@Exclude with toPlainOnly set to true then it should be excluded only during instanceToPlain and classToPlainFromExist operations', () => { 8 | defaultMetadataStorage.clear(); 9 | 10 | class User { 11 | id: number; 12 | firstName: string; 13 | lastName: string; 14 | 15 | @Exclude({ toPlainOnly: true }) 16 | password: string; 17 | } 18 | 19 | const user = new User(); 20 | user.firstName = 'Umed'; 21 | user.lastName = 'Khudoiberdiev'; 22 | user.password = 'imnosuperman'; 23 | 24 | const plainUser = { 25 | firstName: 'Umed', 26 | lastName: 'Khudoiberdiev', 27 | password: 'imnosuperman', 28 | }; 29 | 30 | const plainedUser = instanceToPlain(user); 31 | expect(plainedUser).toEqual({ 32 | firstName: 'Umed', 33 | lastName: 'Khudoiberdiev', 34 | }); 35 | 36 | const classedUser = plainToInstance(User, plainUser); 37 | expect(classedUser).toBeInstanceOf(User); 38 | expect(classedUser).toEqual({ 39 | firstName: 'Umed', 40 | lastName: 'Khudoiberdiev', 41 | password: 'imnosuperman', 42 | }); 43 | }); 44 | 45 | it('@Exclude with toClassOnly set to true then it should be excluded only during plainToInstance and plainToClassFromExist operations', () => { 46 | defaultMetadataStorage.clear(); 47 | 48 | class User { 49 | id: number; 50 | firstName: string; 51 | lastName: string; 52 | 53 | @Exclude({ toClassOnly: true }) 54 | password: string; 55 | } 56 | 57 | const user = new User(); 58 | user.firstName = 'Umed'; 59 | user.lastName = 'Khudoiberdiev'; 60 | user.password = 'imnosuperman'; 61 | 62 | const plainUser = { 63 | firstName: 'Umed', 64 | lastName: 'Khudoiberdiev', 65 | password: 'imnosuperman', 66 | }; 67 | 68 | const classedUser = plainToInstance(User, plainUser); 69 | expect(classedUser).toBeInstanceOf(User); 70 | expect(classedUser).toEqual({ 71 | firstName: 'Umed', 72 | lastName: 'Khudoiberdiev', 73 | }); 74 | 75 | const plainedUser = instanceToPlain(user); 76 | expect(plainedUser).toEqual({ 77 | firstName: 'Umed', 78 | lastName: 'Khudoiberdiev', 79 | password: 'imnosuperman', 80 | }); 81 | }); 82 | 83 | it('@Expose with toClassOnly set to true then it should be excluded only during instanceToPlain and classToPlainFromExist operations', () => { 84 | defaultMetadataStorage.clear(); 85 | 86 | @Exclude() 87 | class User { 88 | @Expose() 89 | firstName: string; 90 | 91 | @Expose() 92 | lastName: string; 93 | 94 | @Expose({ toClassOnly: true }) 95 | password: string; 96 | } 97 | 98 | const user = new User(); 99 | user.firstName = 'Umed'; 100 | user.lastName = 'Khudoiberdiev'; 101 | user.password = 'imnosuperman'; 102 | 103 | const plainUser = { 104 | firstName: 'Umed', 105 | lastName: 'Khudoiberdiev', 106 | password: 'imnosuperman', 107 | }; 108 | 109 | const plainedUser = instanceToPlain(user); 110 | expect(plainedUser).toEqual({ 111 | firstName: 'Umed', 112 | lastName: 'Khudoiberdiev', 113 | }); 114 | 115 | const classedUser = plainToInstance(User, plainUser); 116 | expect(classedUser).toBeInstanceOf(User); 117 | expect(classedUser).toEqual({ 118 | firstName: 'Umed', 119 | lastName: 'Khudoiberdiev', 120 | password: 'imnosuperman', 121 | }); 122 | }); 123 | 124 | it('@Expose with toPlainOnly set to true then it should be excluded only during instanceToPlain and classToPlainFromExist operations', () => { 125 | defaultMetadataStorage.clear(); 126 | 127 | @Exclude() 128 | class User { 129 | @Expose() 130 | firstName: string; 131 | 132 | @Expose() 133 | lastName: string; 134 | 135 | @Expose({ toPlainOnly: true }) 136 | password: string; 137 | } 138 | 139 | const user = new User(); 140 | user.firstName = 'Umed'; 141 | user.lastName = 'Khudoiberdiev'; 142 | user.password = 'imnosuperman'; 143 | 144 | const plainUser = { 145 | firstName: 'Umed', 146 | lastName: 'Khudoiberdiev', 147 | password: 'imnosuperman', 148 | }; 149 | 150 | const plainedUser = instanceToPlain(user); 151 | expect(plainedUser).toEqual({ 152 | firstName: 'Umed', 153 | lastName: 'Khudoiberdiev', 154 | password: 'imnosuperman', 155 | }); 156 | 157 | const classedUser = plainToInstance(User, plainUser); 158 | expect(classedUser).toBeInstanceOf(User); 159 | expect(classedUser).toEqual({ 160 | firstName: 'Umed', 161 | lastName: 'Khudoiberdiev', 162 | }); 163 | }); 164 | 165 | it('should ignore undefined properties when exposeUnsetFields is set to false during class to plain', () => { 166 | defaultMetadataStorage.clear(); 167 | 168 | @Exclude() 169 | class User { 170 | @Expose() 171 | firstName: string; 172 | 173 | @Expose() 174 | lastName: string; 175 | } 176 | 177 | expect(instanceToPlain(new User(), { exposeUnsetFields: false })).toEqual({}); 178 | expect(instanceToPlain(new User(), { exposeUnsetFields: true })).toEqual({ 179 | firstName: undefined, 180 | lastName: undefined, 181 | }); 182 | 183 | const classedUser = plainToInstance(User, { exposeUnsetFields: false }); 184 | expect(classedUser).toBeInstanceOf(User); 185 | expect(classedUser).toEqual({ 186 | firstName: undefined, 187 | lastName: undefined, 188 | }); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /test/functional/transformer-method.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { defaultMetadataStorage } from '../../src/storage'; 3 | import { 4 | Exclude, 5 | Expose, 6 | TransformInstanceToInstance, 7 | TransformInstanceToPlain, 8 | TransformPlainToInstance, 9 | } from '../../src/decorators'; 10 | 11 | describe('transformer methods decorator', () => { 12 | it('should expose non configuration properties and return User instance class', () => { 13 | defaultMetadataStorage.clear(); 14 | 15 | @Exclude() 16 | class User { 17 | id: number; 18 | 19 | @Expose() 20 | firstName: string; 21 | 22 | @Expose() 23 | lastName: string; 24 | 25 | password: string; 26 | } 27 | 28 | class UserController { 29 | @TransformInstanceToInstance() 30 | getUser(): User { 31 | const user = new User(); 32 | user.firstName = 'Snir'; 33 | user.lastName = 'Segal'; 34 | user.password = 'imnosuperman'; 35 | 36 | return user; 37 | } 38 | } 39 | 40 | const controller = new UserController(); 41 | 42 | const result = controller.getUser(); 43 | expect(result.password).toBeUndefined(); 44 | 45 | const plainUser = { 46 | firstName: 'Snir', 47 | lastName: 'Segal', 48 | }; 49 | 50 | expect(result).toEqual(plainUser); 51 | expect(result).toBeInstanceOf(User); 52 | }); 53 | 54 | it('should expose non configuration properties and return User instance class instead of plain object', () => { 55 | defaultMetadataStorage.clear(); 56 | 57 | @Exclude() 58 | class User { 59 | id: number; 60 | 61 | @Expose() 62 | firstName: string; 63 | 64 | @Expose() 65 | lastName: string; 66 | 67 | password: string; 68 | } 69 | 70 | class UserController { 71 | @TransformPlainToInstance(User) 72 | getUser(): User { 73 | const user: any = {}; 74 | user.firstName = 'Snir'; 75 | user.lastName = 'Segal'; 76 | user.password = 'imnosuperman'; 77 | 78 | return user; 79 | } 80 | } 81 | 82 | const controller = new UserController(); 83 | 84 | const result = controller.getUser(); 85 | expect(result.password).toBeUndefined(); 86 | 87 | const user = new User(); 88 | user.firstName = 'Snir'; 89 | user.lastName = 'Segal'; 90 | 91 | expect(result).toEqual(user); 92 | expect(result).toBeInstanceOf(User); 93 | }); 94 | 95 | it('should expose non configuration properties', () => { 96 | defaultMetadataStorage.clear(); 97 | 98 | @Exclude() 99 | class User { 100 | id: number; 101 | 102 | @Expose() 103 | firstName: string; 104 | 105 | @Expose() 106 | lastName: string; 107 | 108 | password: string; 109 | } 110 | 111 | class UserController { 112 | @TransformInstanceToPlain() 113 | getUser(): User { 114 | const user = new User(); 115 | user.firstName = 'Snir'; 116 | user.lastName = 'Segal'; 117 | user.password = 'imnosuperman'; 118 | 119 | return user; 120 | } 121 | } 122 | 123 | const controller = new UserController(); 124 | 125 | const result = controller.getUser(); 126 | expect(result.password).toBeUndefined(); 127 | 128 | const plainUser = { 129 | firstName: 'Snir', 130 | lastName: 'Segal', 131 | }; 132 | 133 | expect(result).toEqual(plainUser); 134 | }); 135 | 136 | it('should expose non configuration properties and properties with specific groups', () => { 137 | defaultMetadataStorage.clear(); 138 | 139 | @Exclude() 140 | class User { 141 | id: number; 142 | 143 | @Expose() 144 | firstName: string; 145 | 146 | @Expose() 147 | lastName: string; 148 | 149 | @Expose({ groups: ['user.permissions'] }) 150 | roles: string[]; 151 | 152 | password: string; 153 | } 154 | 155 | class UserController { 156 | @TransformInstanceToPlain({ groups: ['user.permissions'] }) 157 | getUserWithRoles(): User { 158 | const user = new User(); 159 | user.firstName = 'Snir'; 160 | user.lastName = 'Segal'; 161 | user.password = 'imnosuperman'; 162 | user.roles = ['USER', 'MANAGER']; 163 | 164 | return user; 165 | } 166 | } 167 | 168 | const controller = new UserController(); 169 | 170 | const result = controller.getUserWithRoles(); 171 | expect(result.password).toBeUndefined(); 172 | 173 | const plainUser = { 174 | firstName: 'Snir', 175 | lastName: 'Segal', 176 | roles: ['USER', 'MANAGER'], 177 | }; 178 | 179 | expect(result).toEqual(plainUser); 180 | }); 181 | 182 | it('should expose non configuration properties with specific version', () => { 183 | defaultMetadataStorage.clear(); 184 | 185 | @Exclude() 186 | class User { 187 | id: number; 188 | 189 | @Expose() 190 | firstName: string; 191 | 192 | @Expose() 193 | lastName: string; 194 | 195 | @Expose({ groups: ['user.permissions'] }) 196 | roles: string[]; 197 | 198 | @Expose({ since: 2 }) 199 | websiteUrl?: string; 200 | 201 | password: string; 202 | } 203 | 204 | class UserController { 205 | @TransformInstanceToPlain({ version: 1 }) 206 | getUserVersion1(): User { 207 | const user = new User(); 208 | user.firstName = 'Snir'; 209 | user.lastName = 'Segal'; 210 | user.password = 'imnosuperman'; 211 | user.roles = ['USER', 'MANAGER']; 212 | user.websiteUrl = 'http://www.github.com'; 213 | 214 | return user; 215 | } 216 | 217 | @TransformInstanceToPlain({ version: 2 }) 218 | getUserVersion2(): User { 219 | const user = new User(); 220 | user.firstName = 'Snir'; 221 | user.lastName = 'Segal'; 222 | user.password = 'imnosuperman'; 223 | user.roles = ['USER', 'MANAGER']; 224 | user.websiteUrl = 'http://www.github.com'; 225 | 226 | return user; 227 | } 228 | } 229 | 230 | const controller = new UserController(); 231 | 232 | const resultV2 = controller.getUserVersion2(); 233 | expect(resultV2.password).toBeUndefined(); 234 | expect(resultV2.roles).toBeUndefined(); 235 | 236 | const plainUserV2 = { 237 | firstName: 'Snir', 238 | lastName: 'Segal', 239 | websiteUrl: 'http://www.github.com', 240 | }; 241 | 242 | expect(resultV2).toEqual(plainUserV2); 243 | 244 | const resultV1 = controller.getUserVersion1(); 245 | expect(resultV1.password).toBeUndefined(); 246 | expect(resultV1.roles).toBeUndefined(); 247 | expect(resultV1.websiteUrl).toBeUndefined(); 248 | 249 | const plainUserV1 = { 250 | firstName: 'Snir', 251 | lastName: 'Segal', 252 | }; 253 | 254 | expect(resultV1).toEqual(plainUserV1); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /test/functional/transformer-order.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { plainToInstance } from '../../src/index'; 3 | import { defaultMetadataStorage } from '../../src/storage'; 4 | import { Expose, Transform } from '../../src/decorators'; 5 | 6 | describe('applying several transformations', () => { 7 | beforeEach(() => defaultMetadataStorage.clear()); 8 | afterEach(() => defaultMetadataStorage.clear()); 9 | 10 | it('should keep the order of the applied decorators after several plainToInstance() calls', () => { 11 | class User { 12 | @Transform(() => 'Jonathan') 13 | @Transform(() => 'John') 14 | @Expose() 15 | name: string; 16 | } 17 | 18 | const firstUser = plainToInstance(User, { name: 'Joe' }); 19 | expect(firstUser.name).toEqual('John'); 20 | 21 | // Prior to this pull request [#355](https://github.com/typestack/class-transformer/pull/355) 22 | // the order of the transformations was reversed after every `plainToInstance()` call 23 | // So after consecutive calls `User#name` would be "John" - "Jonathan" - "John" - "Jonathan"... 24 | // This test ensures the last transformation is always the last one to be applied 25 | const secondUser = plainToInstance(User, { name: 'Joe' }); 26 | expect(secondUser.name).toEqual('John'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es2018", 6 | "lib": ["es2018"], 7 | "outDir": "build/node", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "removeComments": false, 13 | "esModuleInterop": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "exclude": ["build", "node_modules", "sample", "**/*.spec.ts", "test/**"] 19 | } -------------------------------------------------------------------------------- /tsconfig.prod.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.prod.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "build/cjs" 6 | }, 7 | } -------------------------------------------------------------------------------- /tsconfig.prod.esm2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.prod.json", 3 | "compilerOptions": { 4 | "module": "ES2015", 5 | "outDir": "build/esm2015", 6 | }, 7 | } -------------------------------------------------------------------------------- /tsconfig.prod.esm5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.prod.json", 3 | "compilerOptions": { 4 | "module": "ES2015", 5 | "target": "ES5", 6 | "outDir": "build/esm5", 7 | }, 8 | } -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "strict": false, 5 | "declaration": false, 6 | }, 7 | } -------------------------------------------------------------------------------- /tsconfig.prod.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.prod.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "build/types", 7 | }, 8 | } -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "strict": false, 5 | "strictPropertyInitialization": false, 6 | "sourceMap": false, 7 | "removeComments": true, 8 | "noImplicitAny": false, 9 | }, 10 | "exclude": ["node_modules"] 11 | } --------------------------------------------------------------------------------