├── .eslintrc.yml ├── .gitbook.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.yml │ ├── Feature_request.yml │ ├── Regression.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql-analysis.yml │ ├── continuous-deployment-workflow.yml │ ├── continuous-integration-workflow.yml │ └── lock-closed-issues-workflow.yml ├── .gitignore ├── .prettierrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── SUMMARY.md └── pages │ ├── 01-getting-started.md │ └── 02-basic-usage.md ├── jest.config.js ├── package-lock.json ├── package.json ├── renovate.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-parameter-properties': off 18 | '@typescript-eslint/explicit-function-return-type': off 19 | '@typescript-eslint/no-explicit-any': off 20 | '@typescript-eslint/member-ordering': 'error' 21 | '@typescript-eslint/no-unused-vars': 22 | - 'error' 23 | - args: 'none' 24 | # TODO: Remove these and fixed issues once we merged all the current PRs. 25 | '@typescript-eslint/ban-types': off 26 | '@typescript-eslint/no-unsafe-return': off 27 | '@typescript-eslint/no-unsafe-assignment': off 28 | '@typescript-eslint/no-unsafe-call': off 29 | '@typescript-eslint/no-unsafe-member-access': off 30 | '@typescript-eslint/no-unsafe-argument': off 31 | '@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/ISSUE_TEMPLATE/Bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug Report" 2 | description: "If something isn't working as expected \U0001F914" 3 | labels: ["needs triage", "bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | **NOTE:** You don't need to answer questions that you know that aren't relevant. 17 | 18 | --- 19 | 20 | - type: checkboxes 21 | attributes: 22 | label: "Is there an existing issue for this?" 23 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the bug you encountered" 24 | options: 25 | - label: "I have searched the existing issues" 26 | required: true 27 | 28 | - type: textarea 29 | validations: 30 | required: true 31 | attributes: 32 | label: "Current behavior" 33 | description: "How the issue manifests?" 34 | 35 | - type: input 36 | validations: 37 | required: true 38 | attributes: 39 | label: "Minimum reproduction code" 40 | description: "An URL to some git repository or gist that reproduces this issue. [Wtf is a minimum reproduction?](https://jmcdo29.github.io/wtf-is-a-minimum-reproduction)" 41 | placeholder: "https://github.com/..." 42 | 43 | - type: textarea 44 | attributes: 45 | label: "Steps to reproduce" 46 | description: | 47 | How the issue manifests? 48 | You could leave this blank if you alread write this in your reproduction code/repo 49 | placeholder: | 50 | 1. `npm i` 51 | 2. `npm start:dev` 52 | 3. See error... 53 | 54 | - type: textarea 55 | validations: 56 | required: true 57 | attributes: 58 | label: "Expected behavior" 59 | description: "A clear and concise description of what you expected to happend (or code)" 60 | 61 | - type: markdown 62 | attributes: 63 | value: | 64 | --- 65 | 66 | - type: input 67 | validations: 68 | required: true 69 | attributes: 70 | label: "Package version" 71 | description: | 72 | Which version of `@nestjs/class-transformer` are you using? 73 | **Tip**: Make sure that all of yours `@nestjs/*` dependencies are in sync! 74 | placeholder: "0.13.1" 75 | 76 | - type: input 77 | attributes: 78 | label: "NestJS version" 79 | description: "Which version of `@nestjs/core` are you using?" 80 | placeholder: "8.1.3" 81 | 82 | - type: input 83 | attributes: 84 | label: "Node.js version" 85 | description: "Which version of Node.js are you using?" 86 | placeholder: "14.17.6" 87 | 88 | - type: checkboxes 89 | attributes: 90 | label: "In which operating systems have you tested?" 91 | options: 92 | - label: macOS 93 | - label: Windows 94 | - label: Linux 95 | 96 | - type: markdown 97 | attributes: 98 | value: | 99 | --- 100 | 101 | - type: textarea 102 | attributes: 103 | label: "Other" 104 | description: | 105 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 106 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 107 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature Request" 2 | description: "I have a suggestion \U0001F63B!" 3 | labels: ["feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | --- 17 | 18 | - type: checkboxes 19 | attributes: 20 | label: "Is there an existing issue that is already proposing this?" 21 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 22 | options: 23 | - label: "I have searched the existing issues" 24 | required: true 25 | 26 | - type: textarea 27 | validations: 28 | required: true 29 | attributes: 30 | label: "Is your feature request related to a problem? Please describe it" 31 | description: "A clear and concise description of what the problem is" 32 | placeholder: | 33 | I have an issue when ... 34 | 35 | - type: textarea 36 | validations: 37 | required: true 38 | attributes: 39 | label: "Describe the solution you'd like" 40 | description: "A clear and concise description of what you want to happen. Add any considered drawbacks" 41 | 42 | - type: textarea 43 | attributes: 44 | label: "Teachability, documentation, adoption, migration strategy" 45 | description: "If you can, explain how users will be able to use this and possibly write out a version the docs. Maybe a screenshot or design?" 46 | 47 | - type: textarea 48 | validations: 49 | required: true 50 | attributes: 51 | label: "What is the motivation / use case for changing the behavior?" 52 | description: "Describe the motivation or the concrete use case" 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Regression.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F4A5 Regression" 2 | description: "Report an unexpected behavior while upgrading your Nest application!" 3 | labels: ["needs triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## :warning: We use GitHub Issues to track bug reports, feature requests and regressions 9 | 10 | If you are not sure that your issue is a bug, you could: 11 | 12 | - use our [Discord community](https://discord.gg/NestJS) 13 | - use [StackOverflow using the tag `nestjs`](https://stackoverflow.com/questions/tagged/nestjs) 14 | - If it's just a quick question you can ping [our Twitter](https://twitter.com/nestframework) 15 | 16 | **NOTE:** You don't need to answer questions that you know that aren't relevant. 17 | 18 | --- 19 | 20 | - type: checkboxes 21 | attributes: 22 | label: "Did you read the migration guide?" 23 | description: "Check out the [migration guide here](https://docs.nestjs.com/migration-guide)!" 24 | options: 25 | - label: "I have read the whole migration guide" 26 | required: false 27 | 28 | - type: checkboxes 29 | attributes: 30 | label: "Is there an existing issue that is already proposing this?" 31 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 32 | options: 33 | - label: "I have searched the existing issues" 34 | required: true 35 | 36 | - type: input 37 | attributes: 38 | label: "Potential Commit/PR that introduced the regression" 39 | description: "If you have time to investigate, what PR/date/version introduced this issue" 40 | placeholder: "PR #123 or commit 5b3c4a4" 41 | 42 | - type: input 43 | attributes: 44 | label: "Versions" 45 | description: "From which version of `@nestjs/class-transformer` to which version you are upgrading" 46 | placeholder: "0.13.0 -> 0.13.1" 47 | 48 | - type: textarea 49 | validations: 50 | required: true 51 | attributes: 52 | label: "Describe the regression" 53 | description: "A clear and concise description of what the regression is" 54 | 55 | - type: textarea 56 | attributes: 57 | label: "Minimum reproduction code" 58 | description: | 59 | Please share a git repo, a gist, or step-by-step instructions. [Wtf is a minimum reproduction?](https://jmcdo29.github.io/wtf-is-a-minimum-reproduction) 60 | **Tip:** If you leave a minimum repository, we will understand your issue faster! 61 | value: | 62 | ```ts 63 | 64 | ``` 65 | 66 | - type: textarea 67 | validations: 68 | required: true 69 | attributes: 70 | label: "Expected behavior" 71 | description: "A clear and concise description of what you expected to happend (or code)" 72 | 73 | - type: textarea 74 | attributes: 75 | label: "Other" 76 | description: | 77 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 78 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 79 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | ## To encourage contributors to use issue templates, we don't allow blank issues 2 | blank_issues_enabled: false 3 | 4 | contact_links: 5 | - name: "\u2753 Discord Community of NestJS" 6 | url: "https://discord.gg/NestJS" 7 | about: "Please ask support questions or discuss suggestions/enhancements here." 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | 3 | Please check if your PR fulfills the following requirements: 4 | 5 | - [ ] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md 6 | - [ ] Tests for the changes have been added (for bug fixes / features) 7 | - [ ] Docs have been added / updated (for bug fixes / features) 8 | 9 | ## PR Type 10 | 11 | What kind of change does this PR introduce? 12 | 13 | 14 | 15 | - [ ] Bugfix 16 | - [ ] Feature 17 | - [ ] Code style update (formatting, local variables) 18 | - [ ] Refactoring (no functional changes, no api changes) 19 | - [ ] Build related changes 20 | - [ ] CI related changes 21 | - [ ] Other... Please describe: 22 | 23 | ## What is the current behavior? 24 | 25 | 26 | 27 | Issue Number: N/A 28 | 29 | ## What is the new behavior? 30 | 31 | ## Does this PR introduce a breaking change? 32 | 33 | - [ ] Yes 34 | - [ ] No 35 | 36 | 37 | 38 | ## Other information 39 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '19 0 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.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@v2 11 | - uses: actions/setup-node@v2 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 --access public ./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@v2 9 | - uses: actions/setup-node@v2 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', '16.x'] 19 | fail-fast: false 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Setting up Node.js (v${{ matrix.node-version }}.x) 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci --ignore-scripts 27 | - run: npm run test:ci 28 | build: 29 | name: Build 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: actions/setup-node@v2 34 | - run: npm ci --ignore-scripts 35 | - run: npm run build:es2015 36 | - run: npm run build:esm5 37 | - run: npm run build:cjs 38 | - run: npm run build:umd 39 | - run: npm run build:types 40 | -------------------------------------------------------------------------------- /.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@v3 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 | 2 | arrowParens: avoid 3 | singleQuote: true 4 | trailingComma: es5 -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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, classToPlain } 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 = classToPlain(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 | - `plainToClass` - transforms a plain object to an instance of the specified class constructor 6 | - `classToPlain` - 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": "@nestjs/class-transformer", 3 | "version": "0.5.1", 4 | "description": "Fork of the class-transformer package. 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/nestjs/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": "^28.0.0", 56 | "@rollup/plugin-node-resolve": "^16.0.0", 57 | "@types/jest": "^27.0.3", 58 | "@types/node": "^22.0.0", 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": "^25.2.4", 64 | "husky": "^9.0.0", 65 | "jest": "^26.6.3", 66 | "lint-staged": "^15.0.0", 67 | "prettier": "^2.4.1", 68 | "reflect-metadata": "0.2.2", 69 | "rimraf": "6.0.1", 70 | "rollup": "^2.60.1", 71 | "rollup-plugin-terser": "^7.0.2", 72 | "ts-jest": "^26.5.6", 73 | "ts-node": "^10.4.0", 74 | "typescript": "^4.5.2" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "semanticCommits": true, 3 | "includeForks": true, 4 | "packageRules": [{ 5 | "depTypeList": ["devDependencies"], 6 | "automerge": true 7 | }], 8 | "extends": [ 9 | "config:base" 10 | ] 11 | } -------------------------------------------------------------------------------- /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( 14 | id: number, 15 | firstName: string, 16 | lastName: string, 17 | password: string 18 | ) { 19 | this.id = id; 20 | this.firstName = firstName; 21 | this.lastName = lastName; 22 | this.password = password; 23 | } 24 | 25 | get name() { 26 | return this.firstName + ' ' + this.lastName; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 { 6 | classToPlain, 7 | plainToClass, 8 | plainToClassFromExist, 9 | } from '../../src/index'; 10 | import { SuperCollection } from './SuperCollection'; 11 | 12 | let collection = new SimpleCollection(); 13 | collection.items = [ 14 | new User(1, 'Johny', 'Cage', '*******'), 15 | new User(2, 'Dima', 'Cage', '*******'), 16 | ]; 17 | collection.count = 2; 18 | 19 | // using generics works only for classToPlain operations, since in runtime we can 20 | // "guess" type without type provided only we have a constructor, not plain object. 21 | 22 | // console.log(classToPlain(collection)); 23 | 24 | // alternatively you can use factory method 25 | 26 | let collectionJson = { 27 | items: [ 28 | { 29 | id: 1, 30 | firstName: 'Johny', 31 | lastName: 'Cage', 32 | password: '*******', 33 | }, 34 | { 35 | id: 2, 36 | firstName: 'Dima', 37 | lastName: 'Cage', 38 | password: '*******', 39 | }, 40 | ], 41 | }; 42 | 43 | console.log( 44 | plainToClassFromExist(new SuperCollection(User), collectionJson) 45 | ); 46 | -------------------------------------------------------------------------------- /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 { TransformOperationExecutor } from './TransformOperationExecutor'; 2 | import { defaultOptions } from './constants/default-options.constant'; 3 | import { TransformationType } from './enums'; 4 | import { ClassConstructor, ClassTransformOptions } from './interfaces'; 5 | 6 | export class ClassTransformer { 7 | // ------------------------------------------------------------------------- 8 | // Public Methods 9 | // ------------------------------------------------------------------------- 10 | 11 | /** 12 | * Converts class (constructor) object to plain (literal) object. Also works with arrays. 13 | */ 14 | instanceToPlain>( 15 | object: T, 16 | options?: ClassTransformOptions 17 | ): Record; 18 | instanceToPlain>( 19 | object: T[], 20 | options?: ClassTransformOptions 21 | ): Record[]; 22 | instanceToPlain>( 23 | object: T | T[], 24 | options?: ClassTransformOptions 25 | ): Record | Record[] { 26 | const executor = new TransformOperationExecutor( 27 | TransformationType.CLASS_TO_PLAIN, 28 | { 29 | ...defaultOptions, 30 | ...options, 31 | } 32 | ); 33 | return executor.transform( 34 | undefined, 35 | object, 36 | undefined, 37 | undefined, 38 | undefined, 39 | undefined 40 | ); 41 | } 42 | 43 | /** 44 | * Converts class (constructor) object to plain (literal) object. 45 | * Uses given plain object as source object (it means fills given plain object with data from class object). 46 | * Also works with arrays. 47 | */ 48 | classToPlainFromExist, P>( 49 | object: T, 50 | plainObject: P, 51 | options?: ClassTransformOptions 52 | ): T; 53 | classToPlainFromExist, P>( 54 | object: T, 55 | plainObjects: P[], 56 | options?: ClassTransformOptions 57 | ): T[]; 58 | classToPlainFromExist, P>( 59 | object: T, 60 | plainObject: P | P[], 61 | options?: ClassTransformOptions 62 | ): T | T[] { 63 | const executor = new TransformOperationExecutor( 64 | TransformationType.CLASS_TO_PLAIN, 65 | { 66 | ...defaultOptions, 67 | ...options, 68 | } 69 | ); 70 | return executor.transform( 71 | plainObject, 72 | object, 73 | undefined, 74 | undefined, 75 | undefined, 76 | undefined 77 | ); 78 | } 79 | 80 | /** 81 | * Converts plain (literal) object to class (constructor) object. Also works with arrays. 82 | */ 83 | plainToInstance, V extends Array>( 84 | cls: ClassConstructor, 85 | plain: V, 86 | options?: ClassTransformOptions 87 | ): T[]; 88 | plainToInstance, V>( 89 | cls: ClassConstructor, 90 | plain: V, 91 | options?: ClassTransformOptions 92 | ): T; 93 | plainToInstance, V>( 94 | cls: ClassConstructor, 95 | plain: V | V[], 96 | options?: ClassTransformOptions 97 | ): T | T[] { 98 | const executor = new TransformOperationExecutor( 99 | TransformationType.PLAIN_TO_CLASS, 100 | { 101 | ...defaultOptions, 102 | ...options, 103 | } 104 | ); 105 | return executor.transform( 106 | undefined, 107 | plain, 108 | cls, 109 | undefined, 110 | undefined, 111 | undefined 112 | ); 113 | } 114 | 115 | /** 116 | * Converts plain (literal) object to class (constructor) object. 117 | * Uses given object as source object (it means fills given object with data from plain object). 118 | * Also works with arrays. 119 | */ 120 | plainToClassFromExist, V extends Array>( 121 | clsObject: T, 122 | plain: V, 123 | options?: ClassTransformOptions 124 | ): T; 125 | plainToClassFromExist, V>( 126 | clsObject: T, 127 | plain: V, 128 | options?: ClassTransformOptions 129 | ): T[]; 130 | plainToClassFromExist, V>( 131 | clsObject: T, 132 | plain: V | V[], 133 | options?: ClassTransformOptions 134 | ): T | T[] { 135 | const executor = new TransformOperationExecutor( 136 | TransformationType.PLAIN_TO_CLASS, 137 | { 138 | ...defaultOptions, 139 | ...options, 140 | } 141 | ); 142 | return executor.transform( 143 | clsObject, 144 | plain, 145 | undefined, 146 | undefined, 147 | undefined, 148 | undefined 149 | ); 150 | } 151 | 152 | /** 153 | * Converts class (constructor) object to new class (constructor) object. Also works with arrays. 154 | */ 155 | instanceToInstance(object: T, options?: ClassTransformOptions): T; 156 | instanceToInstance(object: T[], options?: ClassTransformOptions): T[]; 157 | instanceToInstance( 158 | object: T | T[], 159 | options?: ClassTransformOptions 160 | ): T | T[] { 161 | const executor = new TransformOperationExecutor( 162 | TransformationType.CLASS_TO_CLASS, 163 | { 164 | ...defaultOptions, 165 | ...options, 166 | } 167 | ); 168 | return executor.transform( 169 | undefined, 170 | object, 171 | undefined, 172 | undefined, 173 | undefined, 174 | undefined 175 | ); 176 | } 177 | 178 | /** 179 | * Converts class (constructor) object to plain (literal) object. 180 | * Uses given plain object as source object (it means fills given plain object with data from class object). 181 | * Also works with arrays. 182 | */ 183 | classToClassFromExist( 184 | object: T, 185 | fromObject: T, 186 | options?: ClassTransformOptions 187 | ): T; 188 | classToClassFromExist( 189 | object: T, 190 | fromObjects: T[], 191 | options?: ClassTransformOptions 192 | ): T[]; 193 | classToClassFromExist( 194 | object: T, 195 | fromObject: T | T[], 196 | options?: ClassTransformOptions 197 | ): T | T[] { 198 | const executor = new TransformOperationExecutor( 199 | TransformationType.CLASS_TO_CLASS, 200 | { 201 | ...defaultOptions, 202 | ...options, 203 | } 204 | ); 205 | return executor.transform( 206 | fromObject, 207 | object, 208 | undefined, 209 | undefined, 210 | undefined, 211 | undefined 212 | ); 213 | } 214 | 215 | /** 216 | * Serializes given object to a JSON string. 217 | */ 218 | serialize(object: T, options?: ClassTransformOptions): string; 219 | serialize(object: T[], options?: ClassTransformOptions): string; 220 | serialize(object: T | T[], options?: ClassTransformOptions): string { 221 | return JSON.stringify(this.instanceToPlain(object, options)); 222 | } 223 | 224 | /** 225 | * Deserializes given JSON string to a object of the given class. 226 | */ 227 | deserialize( 228 | cls: ClassConstructor, 229 | json: string, 230 | options?: ClassTransformOptions 231 | ): T { 232 | const jsonObject: T = JSON.parse(json); 233 | return this.plainToInstance(cls, jsonObject, options); 234 | } 235 | 236 | /** 237 | * Deserializes given JSON string to an array of objects of the given class. 238 | */ 239 | deserializeArray( 240 | cls: ClassConstructor, 241 | json: string, 242 | options?: ClassTransformOptions 243 | ): T[] { 244 | const jsonObject: any[] = JSON.parse(json); 245 | return this.plainToInstance(cls, jsonObject, options); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/MetadataStorage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TypeMetadata, 3 | ExposeMetadata, 4 | ExcludeMetadata, 5 | TransformMetadata, 6 | } from './interfaces'; 7 | import { TransformationType } from './enums'; 8 | 9 | /** 10 | * Storage all library metadata. 11 | */ 12 | export class MetadataStorage { 13 | // ------------------------------------------------------------------------- 14 | // Properties 15 | // ------------------------------------------------------------------------- 16 | 17 | private _typeMetadatas = new Map>(); 18 | private _transformMetadatas = new Map< 19 | Function, 20 | Map 21 | >(); 22 | private _exposeMetadatas = new Map>(); 23 | private _excludeMetadatas = new Map>(); 24 | private _ancestorsMap = new Map(); 25 | 26 | // ------------------------------------------------------------------------- 27 | // Adder Methods 28 | // ------------------------------------------------------------------------- 29 | 30 | addTypeMetadata(metadata: TypeMetadata): void { 31 | if (!this._typeMetadatas.has(metadata.target)) { 32 | this._typeMetadatas.set(metadata.target, new Map()); 33 | } 34 | this._typeMetadatas 35 | .get(metadata.target) 36 | .set(metadata.propertyName, metadata); 37 | } 38 | 39 | addTransformMetadata(metadata: TransformMetadata): void { 40 | if (!this._transformMetadatas.has(metadata.target)) { 41 | this._transformMetadatas.set( 42 | metadata.target, 43 | new Map() 44 | ); 45 | } 46 | if ( 47 | !this._transformMetadatas.get(metadata.target).has(metadata.propertyName) 48 | ) { 49 | this._transformMetadatas 50 | .get(metadata.target) 51 | .set(metadata.propertyName, []); 52 | } 53 | this._transformMetadatas 54 | .get(metadata.target) 55 | .get(metadata.propertyName) 56 | .push(metadata); 57 | } 58 | 59 | addExposeMetadata(metadata: ExposeMetadata): void { 60 | if (!this._exposeMetadatas.has(metadata.target)) { 61 | this._exposeMetadatas.set( 62 | metadata.target, 63 | new Map() 64 | ); 65 | } 66 | this._exposeMetadatas 67 | .get(metadata.target) 68 | .set(metadata.propertyName, metadata); 69 | } 70 | 71 | addExcludeMetadata(metadata: ExcludeMetadata): void { 72 | if (!this._excludeMetadatas.has(metadata.target)) { 73 | this._excludeMetadatas.set( 74 | metadata.target, 75 | new Map() 76 | ); 77 | } 78 | this._excludeMetadatas 79 | .get(metadata.target) 80 | .set(metadata.propertyName, metadata); 81 | } 82 | 83 | // ------------------------------------------------------------------------- 84 | // Public Methods 85 | // ------------------------------------------------------------------------- 86 | 87 | findTransformMetadatas( 88 | target: Function, 89 | propertyName: string, 90 | transformationType: TransformationType 91 | ): TransformMetadata[] { 92 | return this.findMetadatas( 93 | this._transformMetadatas, 94 | target, 95 | propertyName 96 | ).filter(metadata => { 97 | if (!metadata.options) return true; 98 | if ( 99 | metadata.options.toClassOnly === true && 100 | metadata.options.toPlainOnly === true 101 | ) 102 | return true; 103 | 104 | if (metadata.options.toClassOnly === true) { 105 | return ( 106 | transformationType === TransformationType.CLASS_TO_CLASS || 107 | transformationType === TransformationType.PLAIN_TO_CLASS 108 | ); 109 | } 110 | if (metadata.options.toPlainOnly === true) { 111 | return transformationType === TransformationType.CLASS_TO_PLAIN; 112 | } 113 | 114 | return true; 115 | }); 116 | } 117 | 118 | findExcludeMetadata(target: Function, propertyName: string): ExcludeMetadata { 119 | return this.findMetadata(this._excludeMetadatas, target, propertyName); 120 | } 121 | 122 | findExposeMetadata(target: Function, propertyName: string): ExposeMetadata { 123 | return this.findMetadata(this._exposeMetadatas, target, propertyName); 124 | } 125 | 126 | findExposeMetadataByCustomName( 127 | target: Function, 128 | name: string 129 | ): ExposeMetadata { 130 | return this.getExposedMetadatas(target).find(metadata => { 131 | return metadata.options && metadata.options.name === name; 132 | }); 133 | } 134 | 135 | findTypeMetadata(target: Function, propertyName: string): TypeMetadata { 136 | return this.findMetadata(this._typeMetadatas, target, propertyName); 137 | } 138 | 139 | getStrategy(target: Function): 'excludeAll' | 'exposeAll' | 'none' { 140 | const excludeMap = this._excludeMetadatas.get(target); 141 | const exclude = excludeMap && excludeMap.get(undefined); 142 | const exposeMap = this._exposeMetadatas.get(target); 143 | const expose = exposeMap && exposeMap.get(undefined); 144 | if ((exclude && expose) || (!exclude && !expose)) return 'none'; 145 | return exclude ? 'excludeAll' : 'exposeAll'; 146 | } 147 | 148 | getExposedMetadatas(target: Function): ExposeMetadata[] { 149 | return this.getMetadata(this._exposeMetadatas, target); 150 | } 151 | 152 | getExcludedMetadatas(target: Function): ExcludeMetadata[] { 153 | return this.getMetadata(this._excludeMetadatas, target); 154 | } 155 | 156 | getExposedProperties( 157 | target: Function, 158 | transformationType: TransformationType 159 | ): string[] { 160 | return this.getExposedMetadatas(target) 161 | .filter(metadata => { 162 | if (!metadata.options) return true; 163 | if ( 164 | metadata.options.toClassOnly === true && 165 | metadata.options.toPlainOnly === true 166 | ) 167 | return true; 168 | 169 | if (metadata.options.toClassOnly === true) { 170 | return ( 171 | transformationType === TransformationType.CLASS_TO_CLASS || 172 | transformationType === TransformationType.PLAIN_TO_CLASS 173 | ); 174 | } 175 | if (metadata.options.toPlainOnly === true) { 176 | return transformationType === TransformationType.CLASS_TO_PLAIN; 177 | } 178 | 179 | return true; 180 | }) 181 | .map(metadata => metadata.propertyName); 182 | } 183 | 184 | getExcludedProperties( 185 | target: Function, 186 | transformationType: TransformationType 187 | ): string[] { 188 | return this.getExcludedMetadatas(target) 189 | .filter(metadata => { 190 | if (!metadata.options) return true; 191 | if ( 192 | metadata.options.toClassOnly === true && 193 | metadata.options.toPlainOnly === true 194 | ) 195 | return true; 196 | 197 | if (metadata.options.toClassOnly === true) { 198 | return ( 199 | transformationType === TransformationType.CLASS_TO_CLASS || 200 | transformationType === TransformationType.PLAIN_TO_CLASS 201 | ); 202 | } 203 | if (metadata.options.toPlainOnly === true) { 204 | return transformationType === TransformationType.CLASS_TO_PLAIN; 205 | } 206 | 207 | return true; 208 | }) 209 | .map(metadata => metadata.propertyName); 210 | } 211 | 212 | clear(): void { 213 | this._typeMetadatas.clear(); 214 | this._exposeMetadatas.clear(); 215 | this._excludeMetadatas.clear(); 216 | this._ancestorsMap.clear(); 217 | } 218 | 219 | // ------------------------------------------------------------------------- 220 | // Private Methods 221 | // ------------------------------------------------------------------------- 222 | 223 | private getMetadata( 224 | metadatas: Map>, 225 | target: Function 226 | ): T[] { 227 | const metadataFromTargetMap = metadatas.get(target); 228 | let metadataFromTarget: T[]; 229 | if (metadataFromTargetMap) { 230 | metadataFromTarget = Array.from(metadataFromTargetMap.values()).filter( 231 | meta => meta.propertyName !== undefined 232 | ); 233 | } 234 | const metadataFromAncestors: T[] = []; 235 | for (const ancestor of this.getAncestors(target)) { 236 | const ancestorMetadataMap = metadatas.get(ancestor); 237 | if (ancestorMetadataMap) { 238 | const metadataFromAncestor = Array.from( 239 | ancestorMetadataMap.values() 240 | ).filter(meta => meta.propertyName !== undefined); 241 | metadataFromAncestors.push(...metadataFromAncestor); 242 | } 243 | } 244 | return metadataFromAncestors.concat(metadataFromTarget || []); 245 | } 246 | 247 | private findMetadata( 248 | metadatas: Map>, 249 | target: Function, 250 | propertyName: string 251 | ): T { 252 | const metadataFromTargetMap = metadatas.get(target); 253 | if (metadataFromTargetMap) { 254 | const metadataFromTarget = metadataFromTargetMap.get(propertyName); 255 | if (metadataFromTarget) { 256 | return metadataFromTarget; 257 | } 258 | } 259 | for (const ancestor of this.getAncestors(target)) { 260 | const ancestorMetadataMap = metadatas.get(ancestor); 261 | if (ancestorMetadataMap) { 262 | const ancestorResult = ancestorMetadataMap.get(propertyName); 263 | if (ancestorResult) { 264 | return ancestorResult; 265 | } 266 | } 267 | } 268 | return undefined; 269 | } 270 | 271 | private findMetadatas( 272 | metadatas: Map>, 273 | target: Function, 274 | propertyName: string 275 | ): T[] { 276 | const metadataFromTargetMap = metadatas.get(target); 277 | let metadataFromTarget: T[]; 278 | if (metadataFromTargetMap) { 279 | metadataFromTarget = metadataFromTargetMap.get(propertyName); 280 | } 281 | const metadataFromAncestorsTarget: T[] = []; 282 | for (const ancestor of this.getAncestors(target)) { 283 | const ancestorMetadataMap = metadatas.get(ancestor); 284 | if (ancestorMetadataMap) { 285 | if (ancestorMetadataMap.has(propertyName)) { 286 | metadataFromAncestorsTarget.push( 287 | ...ancestorMetadataMap.get(propertyName) 288 | ); 289 | } 290 | } 291 | } 292 | return metadataFromAncestorsTarget 293 | .slice() 294 | .reverse() 295 | .concat((metadataFromTarget || []).slice().reverse()); 296 | } 297 | 298 | private getAncestors(target: Function): Function[] { 299 | if (!target) return []; 300 | if (!this._ancestorsMap.has(target)) { 301 | const ancestors: Function[] = []; 302 | for ( 303 | let baseClass = Object.getPrototypeOf(target.prototype.constructor); 304 | typeof baseClass.prototype !== 'undefined'; 305 | baseClass = Object.getPrototypeOf(baseClass.prototype.constructor) 306 | ) { 307 | ancestors.push(baseClass); 308 | } 309 | this._ancestorsMap.set(target, ancestors); 310 | } 311 | return this._ancestorsMap.get(target); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/TransformOperationExecutor.ts: -------------------------------------------------------------------------------- 1 | import { TransformationType } from './enums'; 2 | import { 3 | ClassTransformOptions, 4 | TypeHelpOptions, 5 | TypeMetadata, 6 | TypeOptions, 7 | } from './interfaces'; 8 | import { defaultMetadataStorage } from './storage'; 9 | import { getGlobal, isPromise } from './utils'; 10 | 11 | function instantiateArrayType(arrayType: Function): Array | Set { 12 | const array = new (arrayType as any)(); 13 | if (!(array instanceof Set) && !('push' in array)) { 14 | return []; 15 | } 16 | return array; 17 | } 18 | 19 | export class TransformOperationExecutor { 20 | // ------------------------------------------------------------------------- 21 | // Private Properties 22 | // ------------------------------------------------------------------------- 23 | 24 | private recursionStack = new Set>(); 25 | 26 | // ------------------------------------------------------------------------- 27 | // Constructor 28 | // ------------------------------------------------------------------------- 29 | 30 | constructor( 31 | private transformationType: TransformationType, 32 | private options: ClassTransformOptions 33 | ) {} 34 | 35 | // ------------------------------------------------------------------------- 36 | // Public Methods 37 | // ------------------------------------------------------------------------- 38 | 39 | transform( 40 | source: Record | Record[] | any, 41 | value: Record | Record[] | any, 42 | targetType: Function | TypeMetadata, 43 | arrayType: Function, 44 | isMap: boolean, 45 | level = 0 46 | ): any { 47 | if (Array.isArray(value) || value instanceof Set) { 48 | const newValue = 49 | arrayType && 50 | this.transformationType === TransformationType.PLAIN_TO_CLASS 51 | ? instantiateArrayType(arrayType) 52 | : []; 53 | (value as any[]).forEach((subValue, index) => { 54 | const subSource = source ? source[index] : undefined; 55 | if (!this.options.enableCircularCheck || !this.isCircular(subValue)) { 56 | let realTargetType; 57 | if ( 58 | typeof targetType !== 'function' && 59 | targetType && 60 | targetType.options && 61 | targetType.options.discriminator && 62 | targetType.options.discriminator.property && 63 | targetType.options.discriminator.subTypes 64 | ) { 65 | if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { 66 | realTargetType = targetType.options.discriminator.subTypes.find( 67 | subType => 68 | subType.name === 69 | subValue[ 70 | (targetType as { options: TypeOptions }).options 71 | .discriminator.property 72 | ] 73 | ); 74 | const options: TypeHelpOptions = { 75 | newObject: newValue, 76 | object: subValue, 77 | property: undefined, 78 | }; 79 | const newType = targetType.typeFunction(options); 80 | realTargetType === undefined 81 | ? (realTargetType = newType) 82 | : (realTargetType = realTargetType.value); 83 | if (!targetType.options.keepDiscriminatorProperty) 84 | delete subValue[targetType.options.discriminator.property]; 85 | } 86 | 87 | if (this.transformationType === TransformationType.CLASS_TO_CLASS) { 88 | realTargetType = subValue.constructor; 89 | } 90 | if (this.transformationType === TransformationType.CLASS_TO_PLAIN) { 91 | subValue[targetType.options.discriminator.property] = 92 | targetType.options.discriminator.subTypes.find( 93 | subType => subType.value === subValue.constructor 94 | ).name; 95 | } 96 | } else { 97 | realTargetType = targetType; 98 | } 99 | const value = this.transform( 100 | subSource, 101 | subValue, 102 | realTargetType, 103 | undefined, 104 | subValue instanceof Map, 105 | level + 1 106 | ); 107 | 108 | if (newValue instanceof Set) { 109 | newValue.add(value); 110 | } else { 111 | newValue.push(value); 112 | } 113 | } else if ( 114 | this.transformationType === TransformationType.CLASS_TO_CLASS 115 | ) { 116 | if (newValue instanceof Set) { 117 | newValue.add(subValue); 118 | } else { 119 | newValue.push(subValue); 120 | } 121 | } 122 | }); 123 | return newValue; 124 | } else if (targetType === String && !isMap) { 125 | if (value === null || value === undefined) return value; 126 | return String(value); 127 | } else if (targetType === Number && !isMap) { 128 | if (value === null || value === undefined) return value; 129 | return Number(value); 130 | } else if (targetType === Boolean && !isMap) { 131 | if (value === null || value === undefined) return value; 132 | return Boolean(value); 133 | } else if ((targetType === Date || value instanceof Date) && !isMap) { 134 | if (value instanceof Date) { 135 | return new Date(value.valueOf()); 136 | } 137 | if (value === null || value === undefined) return value; 138 | return new Date(value); 139 | } else if ( 140 | !!getGlobal().Buffer && 141 | (targetType === Buffer || value instanceof Buffer) && 142 | !isMap 143 | ) { 144 | if (value === null || value === undefined) return value; 145 | return Buffer.from(value); 146 | } else if (isPromise(value) && !isMap) { 147 | return new Promise((resolve, reject) => { 148 | value.then( 149 | (data: any) => 150 | resolve( 151 | this.transform( 152 | undefined, 153 | data, 154 | targetType, 155 | undefined, 156 | undefined, 157 | level + 1 158 | ) 159 | ), 160 | reject 161 | ); 162 | }); 163 | } else if ( 164 | !isMap && 165 | value !== null && 166 | typeof value === 'object' && 167 | typeof value.then === 'function' 168 | ) { 169 | // Note: We should not enter this, as promise has been handled above 170 | // This option simply returns the Promise preventing a JS error from happening and should be an inaccessible path. 171 | return value; // skip promise transformation 172 | } else if (typeof value === 'object' && value !== null) { 173 | // try to guess the type 174 | if ( 175 | !targetType && 176 | value.constructor !== 177 | Object /* && TransformationType === TransformationType.CLASS_TO_PLAIN*/ 178 | ) 179 | if (!Array.isArray(value) && value.constructor === Array) { 180 | // Somebody attempts to convert special Array like object to Array, eg: 181 | // const evilObject = { '100000000': '100000000', __proto__: [] }; 182 | // This could be used to cause Denial-of-service attack so we don't allow it. 183 | // See prevent-array-bomb.spec.ts for more details. 184 | } else { 185 | // We are good we can use the built-in constructor 186 | targetType = value.constructor; 187 | } 188 | if (!targetType && source) targetType = source.constructor; 189 | 190 | if (this.options.enableCircularCheck) { 191 | // add transformed type to prevent circular references 192 | this.recursionStack.add(value); 193 | } 194 | 195 | const keys = this.getKeys(targetType as Function, value, isMap); 196 | let newValue: any = source ? source : {}; 197 | if ( 198 | !source && 199 | (this.transformationType === TransformationType.PLAIN_TO_CLASS || 200 | this.transformationType === TransformationType.CLASS_TO_CLASS) 201 | ) { 202 | if (isMap) { 203 | newValue = new Map(); 204 | } else if (targetType) { 205 | newValue = new (targetType as any)(); 206 | } else { 207 | newValue = {}; 208 | } 209 | } 210 | 211 | // traverse over keys 212 | for (const key of keys) { 213 | if (key === '__proto__' || key === 'constructor') { 214 | continue; 215 | } 216 | 217 | const valueKey = key; 218 | let newValueKey = key, 219 | propertyName = key; 220 | if (!this.options.ignoreDecorators && targetType) { 221 | if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { 222 | const exposeMetadata = 223 | defaultMetadataStorage.findExposeMetadataByCustomName( 224 | targetType as Function, 225 | key 226 | ); 227 | if (exposeMetadata) { 228 | propertyName = exposeMetadata.propertyName; 229 | newValueKey = exposeMetadata.propertyName; 230 | } 231 | } else if ( 232 | this.transformationType === TransformationType.CLASS_TO_PLAIN || 233 | this.transformationType === TransformationType.CLASS_TO_CLASS 234 | ) { 235 | const exposeMetadata = defaultMetadataStorage.findExposeMetadata( 236 | targetType as Function, 237 | key 238 | ); 239 | if ( 240 | exposeMetadata && 241 | exposeMetadata.options && 242 | exposeMetadata.options.name 243 | ) { 244 | newValueKey = exposeMetadata.options.name; 245 | } 246 | } 247 | } 248 | 249 | // get a subvalue 250 | let subValue: any = undefined; 251 | if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { 252 | /** 253 | * This section is added for the following report: 254 | * https://github.com/typestack/class-transformer/issues/596 255 | * 256 | * We should not call functions or constructors when transforming to class. 257 | */ 258 | subValue = value[valueKey]; 259 | } else { 260 | if (value instanceof Map) { 261 | subValue = value.get(valueKey); 262 | } else if (value[valueKey] instanceof Function) { 263 | subValue = value[valueKey](); 264 | } else { 265 | subValue = value[valueKey]; 266 | } 267 | } 268 | 269 | // determine a type 270 | let type: any = undefined, 271 | isSubValueMap = subValue instanceof Map; 272 | if (targetType && isMap) { 273 | type = targetType; 274 | } else if (targetType) { 275 | const metadata = defaultMetadataStorage.findTypeMetadata( 276 | targetType as Function, 277 | propertyName 278 | ); 279 | if (metadata) { 280 | const options: TypeHelpOptions = { 281 | newObject: newValue, 282 | object: value, 283 | property: propertyName, 284 | }; 285 | const newType = metadata.typeFunction 286 | ? metadata.typeFunction(options) 287 | : metadata.reflectedType; 288 | if ( 289 | metadata.options && 290 | metadata.options.discriminator && 291 | metadata.options.discriminator.property && 292 | metadata.options.discriminator.subTypes 293 | ) { 294 | if (!(value[valueKey] instanceof Array)) { 295 | if ( 296 | this.transformationType === TransformationType.PLAIN_TO_CLASS 297 | ) { 298 | type = metadata.options.discriminator.subTypes.find( 299 | subType => { 300 | if ( 301 | subValue && 302 | subValue instanceof Object && 303 | metadata.options.discriminator.property in subValue 304 | ) { 305 | return ( 306 | subType.name === 307 | subValue[metadata.options.discriminator.property] 308 | ); 309 | } 310 | } 311 | ); 312 | type === undefined ? (type = newType) : (type = type.value); 313 | if (!metadata.options.keepDiscriminatorProperty) { 314 | if ( 315 | subValue && 316 | subValue instanceof Object && 317 | metadata.options.discriminator.property in subValue 318 | ) { 319 | delete subValue[metadata.options.discriminator.property]; 320 | } 321 | } 322 | } 323 | if ( 324 | this.transformationType === TransformationType.CLASS_TO_CLASS 325 | ) { 326 | type = subValue.constructor; 327 | } 328 | if ( 329 | this.transformationType === TransformationType.CLASS_TO_PLAIN 330 | ) { 331 | if (subValue) { 332 | subValue[metadata.options.discriminator.property] = 333 | metadata.options.discriminator.subTypes.find( 334 | subType => subType.value === subValue.constructor 335 | ).name; 336 | } 337 | } 338 | } else { 339 | type = metadata; 340 | } 341 | } else { 342 | type = newType; 343 | } 344 | isSubValueMap = isSubValueMap || metadata.reflectedType === Map; 345 | } else if (this.options.targetMaps) { 346 | // try to find a type in target maps 347 | this.options.targetMaps 348 | .filter( 349 | map => 350 | map.target === targetType && !!map.properties[propertyName] 351 | ) 352 | .forEach(map => (type = map.properties[propertyName])); 353 | } else if ( 354 | this.options.enableImplicitConversion && 355 | this.transformationType === TransformationType.PLAIN_TO_CLASS 356 | ) { 357 | // if we have no registererd type via the @Type() decorator then we check if we have any 358 | // type declarations in reflect-metadata (type declaration is emited only if some decorator is added to the property.) 359 | const reflectedType = (Reflect as any).getMetadata( 360 | 'design:type', 361 | (targetType as Function).prototype, 362 | propertyName 363 | ); 364 | 365 | if (reflectedType) { 366 | type = reflectedType; 367 | } 368 | } 369 | } 370 | 371 | // if value is an array try to get its custom array type 372 | const arrayType = Array.isArray(value[valueKey]) 373 | ? this.getReflectedType(targetType as Function, propertyName) 374 | : undefined; 375 | 376 | // const subValueKey = TransformationType === TransformationType.PLAIN_TO_CLASS && newKeyName ? newKeyName : key; 377 | const subSource = source ? source[valueKey] : undefined; 378 | 379 | // if its deserialization then type if required 380 | // if we uncomment this types like string[] will not work 381 | // if (this.transformationType === TransformationType.PLAIN_TO_CLASS && !type && subValue instanceof Object && !(subValue instanceof Date)) 382 | // throw new Error(`Cannot determine type for ${(targetType as any).name }.${propertyName}, did you forget to specify a @Type?`); 383 | 384 | // if newValue is a source object that has method that match newKeyName then skip it 385 | if (newValue.constructor.prototype) { 386 | const descriptor = Object.getOwnPropertyDescriptor( 387 | newValue.constructor.prototype, 388 | newValueKey 389 | ); 390 | if ( 391 | (this.transformationType === TransformationType.PLAIN_TO_CLASS || 392 | this.transformationType === TransformationType.CLASS_TO_CLASS) && 393 | // eslint-disable-next-line @typescript-eslint/unbound-method 394 | ((descriptor && !descriptor.set) || 395 | newValue[newValueKey] instanceof Function) 396 | ) 397 | // || TransformationType === TransformationType.CLASS_TO_CLASS 398 | continue; 399 | } 400 | 401 | if (!this.options.enableCircularCheck || !this.isCircular(subValue)) { 402 | const transformKey = 403 | this.transformationType === TransformationType.PLAIN_TO_CLASS 404 | ? newValueKey 405 | : key; 406 | let finalValue; 407 | 408 | if (this.transformationType === TransformationType.CLASS_TO_PLAIN) { 409 | // Get original value 410 | finalValue = value[transformKey]; 411 | // Apply custom transformation 412 | finalValue = this.applyCustomTransformations( 413 | finalValue, 414 | targetType as Function, 415 | transformKey, 416 | value, 417 | this.transformationType 418 | ); 419 | // If nothing change, it means no custom transformation was applied, so use the subValue. 420 | finalValue = 421 | value[transformKey] === finalValue ? subValue : finalValue; 422 | // Apply the default transformation 423 | finalValue = this.transform( 424 | subSource, 425 | finalValue, 426 | type, 427 | arrayType, 428 | isSubValueMap, 429 | level + 1 430 | ); 431 | } else { 432 | if (subValue === undefined && this.options.exposeDefaultValues) { 433 | // Set default value if nothing provided 434 | finalValue = newValue[newValueKey]; 435 | } else { 436 | finalValue = this.transform( 437 | subSource, 438 | subValue, 439 | type, 440 | arrayType, 441 | isSubValueMap, 442 | level + 1 443 | ); 444 | finalValue = this.applyCustomTransformations( 445 | finalValue, 446 | targetType as Function, 447 | transformKey, 448 | value, 449 | this.transformationType 450 | ); 451 | } 452 | } 453 | 454 | if (finalValue !== undefined || this.options.exposeUnsetFields) { 455 | if (newValue instanceof Map) { 456 | newValue.set(newValueKey, finalValue); 457 | } else { 458 | newValue[newValueKey] = finalValue; 459 | } 460 | } 461 | } else if ( 462 | this.transformationType === TransformationType.CLASS_TO_CLASS 463 | ) { 464 | let finalValue = subValue; 465 | finalValue = this.applyCustomTransformations( 466 | finalValue, 467 | targetType as Function, 468 | key, 469 | value, 470 | this.transformationType 471 | ); 472 | if (finalValue !== undefined || this.options.exposeUnsetFields) { 473 | if (newValue instanceof Map) { 474 | newValue.set(newValueKey, finalValue); 475 | } else { 476 | newValue[newValueKey] = finalValue; 477 | } 478 | } 479 | } 480 | } 481 | 482 | if (this.options.enableCircularCheck) { 483 | this.recursionStack.delete(value); 484 | } 485 | 486 | return newValue; 487 | } else { 488 | return value; 489 | } 490 | } 491 | 492 | private applyCustomTransformations( 493 | value: any, 494 | target: Function, 495 | key: string, 496 | obj: any, 497 | transformationType: TransformationType 498 | ): boolean { 499 | let metadatas = defaultMetadataStorage.findTransformMetadatas( 500 | target, 501 | key, 502 | this.transformationType 503 | ); 504 | 505 | // apply versioning options 506 | if (this.options.version !== undefined) { 507 | metadatas = metadatas.filter(metadata => { 508 | if (!metadata.options) return true; 509 | 510 | return this.checkVersion( 511 | metadata.options.since, 512 | metadata.options.until 513 | ); 514 | }); 515 | } 516 | 517 | // apply grouping options 518 | if (this.options.groups && this.options.groups.length) { 519 | metadatas = metadatas.filter(metadata => { 520 | if (!metadata.options) return true; 521 | 522 | return this.checkGroups(metadata.options.groups); 523 | }); 524 | } else { 525 | metadatas = metadatas.filter(metadata => { 526 | return ( 527 | !metadata.options || 528 | !metadata.options.groups || 529 | !metadata.options.groups.length 530 | ); 531 | }); 532 | } 533 | 534 | metadatas.forEach(metadata => { 535 | value = metadata.transformFn({ 536 | value, 537 | key, 538 | obj, 539 | type: transformationType, 540 | options: this.options, 541 | }); 542 | }); 543 | 544 | return value; 545 | } 546 | 547 | // preventing circular references 548 | private isCircular(object: Record): boolean { 549 | return this.recursionStack.has(object); 550 | } 551 | 552 | private getReflectedType( 553 | target: Function, 554 | propertyName: string 555 | ): Function | undefined { 556 | if (!target) return undefined; 557 | const meta = defaultMetadataStorage.findTypeMetadata(target, propertyName); 558 | return meta ? meta.reflectedType : undefined; 559 | } 560 | 561 | private getKeys( 562 | target: Function, 563 | object: Record, 564 | isMap: boolean 565 | ): string[] { 566 | // determine exclusion strategy 567 | let strategy = defaultMetadataStorage.getStrategy(target); 568 | if (strategy === 'none') strategy = this.options.strategy || 'exposeAll'; // exposeAll is default strategy 569 | 570 | // get all keys that need to expose 571 | let keys: any[] = []; 572 | if (strategy === 'exposeAll' || isMap) { 573 | if (object instanceof Map) { 574 | keys = Array.from(object.keys()); 575 | } else { 576 | keys = Object.keys(object); 577 | } 578 | } 579 | 580 | if (isMap) { 581 | // expose & exclude do not apply for map keys only to fields 582 | return keys; 583 | } 584 | 585 | /** 586 | * If decorators are ignored but we don't want the extraneous values, then we use the 587 | * metadata to decide which property is needed, but doesn't apply the decorator effect. 588 | */ 589 | if ( 590 | this.options.ignoreDecorators && 591 | this.options.excludeExtraneousValues && 592 | target 593 | ) { 594 | const exposedProperties = defaultMetadataStorage.getExposedProperties( 595 | target, 596 | this.transformationType 597 | ); 598 | const excludedProperties = defaultMetadataStorage.getExcludedProperties( 599 | target, 600 | this.transformationType 601 | ); 602 | keys = [...exposedProperties, ...excludedProperties]; 603 | } 604 | 605 | if (!this.options.ignoreDecorators && target) { 606 | // add all exposed to list of keys 607 | let exposedProperties = defaultMetadataStorage.getExposedProperties( 608 | target, 609 | this.transformationType 610 | ); 611 | if (this.transformationType === TransformationType.PLAIN_TO_CLASS) { 612 | exposedProperties = exposedProperties.map(key => { 613 | const exposeMetadata = defaultMetadataStorage.findExposeMetadata( 614 | target, 615 | key 616 | ); 617 | if ( 618 | exposeMetadata && 619 | exposeMetadata.options && 620 | exposeMetadata.options.name 621 | ) { 622 | return exposeMetadata.options.name; 623 | } 624 | 625 | return key; 626 | }); 627 | } 628 | if (this.options.excludeExtraneousValues) { 629 | keys = exposedProperties; 630 | } else { 631 | keys = keys.concat(exposedProperties); 632 | } 633 | 634 | // exclude excluded properties 635 | const excludedProperties = defaultMetadataStorage.getExcludedProperties( 636 | target, 637 | this.transformationType 638 | ); 639 | if (excludedProperties.length > 0) { 640 | keys = keys.filter(key => { 641 | return !excludedProperties.includes(key); 642 | }); 643 | } 644 | 645 | // apply versioning options 646 | if (this.options.version !== undefined) { 647 | keys = keys.filter(key => { 648 | const exposeMetadata = defaultMetadataStorage.findExposeMetadata( 649 | target, 650 | key 651 | ); 652 | if (!exposeMetadata || !exposeMetadata.options) return true; 653 | 654 | return this.checkVersion( 655 | exposeMetadata.options.since, 656 | exposeMetadata.options.until 657 | ); 658 | }); 659 | } 660 | 661 | // apply grouping options 662 | if (this.options.groups && this.options.groups.length) { 663 | keys = keys.filter(key => { 664 | const exposeMetadata = defaultMetadataStorage.findExposeMetadata( 665 | target, 666 | key 667 | ); 668 | if (!exposeMetadata || !exposeMetadata.options) return true; 669 | 670 | return this.checkGroups(exposeMetadata.options.groups); 671 | }); 672 | } else { 673 | keys = keys.filter(key => { 674 | const exposeMetadata = defaultMetadataStorage.findExposeMetadata( 675 | target, 676 | key 677 | ); 678 | return ( 679 | !exposeMetadata || 680 | !exposeMetadata.options || 681 | !exposeMetadata.options.groups || 682 | !exposeMetadata.options.groups.length 683 | ); 684 | }); 685 | } 686 | } 687 | 688 | // exclude prefixed properties 689 | if (this.options.excludePrefixes && this.options.excludePrefixes.length) { 690 | keys = keys.filter(key => 691 | this.options.excludePrefixes.every(prefix => { 692 | return key.substr(0, prefix.length) !== prefix; 693 | }) 694 | ); 695 | } 696 | 697 | // make sure we have unique keys 698 | keys = keys.filter((key, index, self) => { 699 | return self.indexOf(key) === index; 700 | }); 701 | 702 | return keys; 703 | } 704 | 705 | private checkVersion(since: number, until: number): boolean { 706 | let decision = true; 707 | if (decision && since) decision = this.options.version >= since; 708 | if (decision && until) decision = this.options.version < until; 709 | 710 | return decision; 711 | } 712 | 713 | private checkGroups(groups: string[]): boolean { 714 | if (!groups) return true; 715 | 716 | return this.options.groups.some(optionGroup => 717 | groups.includes(optionGroup) 718 | ); 719 | } 720 | } 721 | -------------------------------------------------------------------------------- /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( 12 | options: ExcludeOptions = {} 13 | ): PropertyDecorator & ClassDecorator { 14 | /** 15 | * NOTE: The `propertyName` property must be marked as optional because 16 | * this decorator used both as a class and a property decorator and the 17 | * Typescript compiler will freak out if we make it mandatory as a class 18 | * decorator only receives one parameter. 19 | */ 20 | return function (object: any, propertyName?: string | Symbol): void { 21 | defaultMetadataStorage.addExcludeMetadata({ 22 | target: object instanceof Function ? object : object.constructor, 23 | propertyName: propertyName as string, 24 | options, 25 | }); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /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( 12 | options: ExposeOptions = {} 13 | ): PropertyDecorator & ClassDecorator { 14 | /** 15 | * NOTE: The `propertyName` property must be marked as optional because 16 | * this decorator used both as a class and a property decorator and the 17 | * Typescript compiler will freak out if we make it mandatory as a class 18 | * decorator only receives one parameter. 19 | */ 20 | return function (object: any, propertyName?: string | Symbol): void { 21 | defaultMetadataStorage.addExposeMetadata({ 22 | target: object instanceof Function ? object : object.constructor, 23 | propertyName: propertyName as string, 24 | options, 25 | }); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /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( 10 | params?: ClassTransformOptions 11 | ): MethodDecorator { 12 | return function ( 13 | target: Record, 14 | propertyKey: string | Symbol, 15 | descriptor: PropertyDescriptor 16 | ): void { 17 | const classTransformer: ClassTransformer = new ClassTransformer(); 18 | const originalMethod = descriptor.value; 19 | 20 | descriptor.value = function (...args: any[]): Record { 21 | const result: any = originalMethod.apply(this, args); 22 | const isPromise = 23 | !!result && 24 | (typeof result === 'object' || typeof result === 'function') && 25 | typeof result.then === 'function'; 26 | return isPromise 27 | ? result.then((data: any) => 28 | classTransformer.instanceToInstance(data, params) 29 | ) 30 | : classTransformer.instanceToInstance(result, params); 31 | }; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /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( 10 | params?: ClassTransformOptions 11 | ): MethodDecorator { 12 | return function ( 13 | target: Record, 14 | propertyKey: string | Symbol, 15 | descriptor: PropertyDescriptor 16 | ): void { 17 | const classTransformer: ClassTransformer = new ClassTransformer(); 18 | const originalMethod = descriptor.value; 19 | 20 | descriptor.value = function (...args: any[]): Record { 21 | const result: any = originalMethod.apply(this, args); 22 | const isPromise = 23 | !!result && 24 | (typeof result === 'object' || typeof result === 'function') && 25 | typeof result.then === 'function'; 26 | return isPromise 27 | ? result.then((data: any) => 28 | classTransformer.instanceToPlain(data, params) 29 | ) 30 | : classTransformer.instanceToPlain(result, params); 31 | }; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/decorators/transform-plain-to-instance.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ClassTransformer } from '../ClassTransformer'; 2 | import { ClassConstructor, 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 TransformPlainToInstance( 10 | classType: ClassConstructor, 11 | params?: ClassTransformOptions 12 | ): MethodDecorator { 13 | return function ( 14 | target: Record, 15 | propertyKey: string | Symbol, 16 | descriptor: PropertyDescriptor 17 | ): void { 18 | const classTransformer: ClassTransformer = new ClassTransformer(); 19 | const originalMethod = descriptor.value; 20 | 21 | descriptor.value = function (...args: any[]): Record { 22 | const result: any = originalMethod.apply(this, args); 23 | const isPromise = 24 | !!result && 25 | (typeof result === 'object' || typeof result === 'function') && 26 | typeof result.then === 'function'; 27 | return isPromise 28 | ? result.then((data: any) => 29 | classTransformer.plainToInstance(classType, data, params) 30 | ) 31 | : classTransformer.plainToInstance(classType, result, params); 32 | }; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /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( 16 | 'design:type', 17 | target, 18 | propertyName 19 | ); 20 | defaultMetadataStorage.addTypeMetadata({ 21 | target: target.constructor, 22 | propertyName: propertyName as string, 23 | reflectedType, 24 | typeFunction, 25 | options, 26 | }); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /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 { ClassConstructor, ClassTransformOptions } from './interfaces'; 3 | 4 | export { ClassTransformer } from './ClassTransformer'; 5 | export * from './decorators'; 6 | export * from './enums'; 7 | export * from './interfaces'; 8 | 9 | const classTransformer = new ClassTransformer(); 10 | 11 | /** 12 | * Converts class (constructor) object to plain (literal) object. Also works with arrays. 13 | * 14 | * @deprecated Function name changed, use the `instanceToPlain` method instead. 15 | */ 16 | export function classToPlain( 17 | object: T, 18 | options?: ClassTransformOptions 19 | ): Record; 20 | export function classToPlain( 21 | object: T[], 22 | options?: ClassTransformOptions 23 | ): Record[]; 24 | export function classToPlain( 25 | object: T | T[], 26 | options?: ClassTransformOptions 27 | ): Record | Record[] { 28 | return classTransformer.instanceToPlain(object, options); 29 | } 30 | 31 | /** 32 | * Converts class (constructor) object to plain (literal) object. Also works with arrays. 33 | */ 34 | export function instanceToPlain( 35 | object: T, 36 | options?: ClassTransformOptions 37 | ): Record; 38 | export function instanceToPlain( 39 | object: T[], 40 | options?: ClassTransformOptions 41 | ): Record[]; 42 | export function instanceToPlain( 43 | object: T | T[], 44 | options?: ClassTransformOptions 45 | ): Record | Record[] { 46 | return classTransformer.instanceToPlain(object, options); 47 | } 48 | 49 | /** 50 | * Converts class (constructor) object to plain (literal) object. 51 | * Uses given plain object as source object (it means fills given plain object with data from class object). 52 | * Also works with arrays. 53 | * 54 | * @deprecated This function is being removed. 55 | */ 56 | export function classToPlainFromExist( 57 | object: T, 58 | plainObject: Record, 59 | options?: ClassTransformOptions 60 | ): Record; 61 | export function classToPlainFromExist( 62 | object: T, 63 | plainObjects: Record[], 64 | options?: ClassTransformOptions 65 | ): Record[]; 66 | export function classToPlainFromExist( 67 | object: T, 68 | plainObject: Record | Record[], 69 | options?: ClassTransformOptions 70 | ): Record | Record[] { 71 | return classTransformer.classToPlainFromExist(object, plainObject, options); 72 | } 73 | 74 | /** 75 | * Converts plain (literal) object to class (constructor) object. Also works with arrays. 76 | * 77 | * @deprecated Function name changed, use the `plainToInstance` method instead. 78 | */ 79 | export function plainToClass( 80 | cls: ClassConstructor, 81 | plain: V[], 82 | options?: ClassTransformOptions 83 | ): T[]; 84 | export function plainToClass( 85 | cls: ClassConstructor, 86 | plain: V, 87 | options?: ClassTransformOptions 88 | ): T; 89 | export function plainToClass( 90 | cls: ClassConstructor, 91 | plain: V | V[], 92 | options?: ClassTransformOptions 93 | ): T | T[] { 94 | return classTransformer.plainToInstance(cls, plain as any, options); 95 | } 96 | 97 | /** 98 | * Converts plain (literal) object to class (constructor) object. Also works with arrays. 99 | */ 100 | export function plainToInstance( 101 | cls: ClassConstructor, 102 | plain: V[], 103 | options?: ClassTransformOptions 104 | ): T[]; 105 | export function plainToInstance( 106 | cls: ClassConstructor, 107 | plain: V, 108 | options?: ClassTransformOptions 109 | ): T; 110 | export function plainToInstance( 111 | cls: ClassConstructor, 112 | plain: V | V[], 113 | options?: ClassTransformOptions 114 | ): T | T[] { 115 | return classTransformer.plainToInstance(cls, plain as any, options); 116 | } 117 | 118 | /** 119 | * Converts plain (literal) object to class (constructor) object. 120 | * Uses given object as source object (it means fills given object with data from plain object). 121 | * Also works with arrays. 122 | * 123 | * @deprecated This function is being removed. The current implementation is incorrect as it modifies the source object. 124 | */ 125 | export function plainToClassFromExist( 126 | clsObject: T[], 127 | plain: V[], 128 | options?: ClassTransformOptions 129 | ): T[]; 130 | export function plainToClassFromExist( 131 | clsObject: T, 132 | plain: V, 133 | options?: ClassTransformOptions 134 | ): T; 135 | export function plainToClassFromExist( 136 | clsObject: T, 137 | plain: V | V[], 138 | options?: ClassTransformOptions 139 | ): T | T[] { 140 | return classTransformer.plainToClassFromExist(clsObject, plain, options); 141 | } 142 | 143 | /** 144 | * Converts class (constructor) object to new class (constructor) object. Also works with arrays. 145 | */ 146 | export function instanceToInstance( 147 | object: T, 148 | options?: ClassTransformOptions 149 | ): T; 150 | export function instanceToInstance( 151 | object: T[], 152 | options?: ClassTransformOptions 153 | ): T[]; 154 | export function instanceToInstance( 155 | object: T | T[], 156 | options?: ClassTransformOptions 157 | ): T | T[] { 158 | return classTransformer.instanceToInstance(object, options); 159 | } 160 | 161 | /** 162 | * Converts class (constructor) object to plain (literal) object. 163 | * Uses given plain object as source object (it means fills given plain object with data from class object). 164 | * Also works with arrays. 165 | * 166 | * @deprecated This function is being removed. The current implementation is incorrect as it modifies the source object. 167 | */ 168 | export function classToClassFromExist( 169 | object: T, 170 | fromObject: T, 171 | options?: ClassTransformOptions 172 | ): T; 173 | export function classToClassFromExist( 174 | object: T, 175 | fromObjects: T[], 176 | options?: ClassTransformOptions 177 | ): T[]; 178 | export function classToClassFromExist( 179 | object: T, 180 | fromObject: T | T[], 181 | options?: ClassTransformOptions 182 | ): T | T[] { 183 | return classTransformer.classToClassFromExist(object, fromObject, options); 184 | } 185 | 186 | /** 187 | * Serializes given object to a JSON string. 188 | * 189 | * @deprecated This function is being removed. Please use 190 | * ``` 191 | * JSON.stringify(instanceToPlain(object, options)) 192 | * ``` 193 | */ 194 | export function serialize( 195 | object: T, 196 | options?: ClassTransformOptions 197 | ): string; 198 | export function serialize( 199 | object: T[], 200 | options?: ClassTransformOptions 201 | ): string; 202 | export function serialize( 203 | object: T | T[], 204 | options?: ClassTransformOptions 205 | ): string { 206 | return classTransformer.serialize(object, options); 207 | } 208 | 209 | /** 210 | * Deserializes given JSON string to a object of the given class. 211 | * 212 | * @deprecated This function is being removed. Please use the following instead: 213 | * ``` 214 | * instanceToClass(cls, JSON.parse(json), options) 215 | * ``` 216 | */ 217 | export function deserialize( 218 | cls: ClassConstructor, 219 | json: string, 220 | options?: ClassTransformOptions 221 | ): T { 222 | return classTransformer.deserialize(cls, json, options); 223 | } 224 | 225 | /** 226 | * Deserializes given JSON string to an array of objects of the given class. 227 | * 228 | * @deprecated This function is being removed. Please use the following instead: 229 | * ``` 230 | * JSON.parse(json).map(value => instanceToClass(cls, value, options)) 231 | * ``` 232 | * 233 | */ 234 | export function deserializeArray( 235 | cls: ClassConstructor, 236 | json: string, 237 | options?: ClassTransformOptions 238 | ): T[] { 239 | return classTransformer.deserializeArray(cls, json, options); 240 | } 241 | -------------------------------------------------------------------------------- /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 | * Expose this property only when transforming from plain to class instance. 7 | */ 8 | toClassOnly?: boolean; 9 | 10 | /** 11 | * Expose 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; 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 | 3 | /** 4 | * Default metadata storage is used as singleton and can be used to storage all metadatas. 5 | */ 6 | export const defaultMetadataStorage = new MetadataStorage(); 7 | -------------------------------------------------------------------------------- /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 { TransformOperationExecutor } from '../../src/TransformOperationExecutor'; 3 | import { 4 | instanceToInstance, 5 | instanceToPlain, 6 | plainToInstance, 7 | } from '../../src/index'; 8 | import { defaultMetadataStorage } from '../../src/storage'; 9 | 10 | describe('circular reference problem', () => { 11 | it('should skip circular reference objects in instanceToPlain operation', () => { 12 | defaultMetadataStorage.clear(); 13 | 14 | class Caption { 15 | text: string; 16 | } 17 | 18 | class Photo { 19 | id: number; 20 | filename: string; 21 | user: User; 22 | users: User[]; 23 | caption: Caption; 24 | } 25 | 26 | class User { 27 | id: number; 28 | firstName: string; 29 | caption: Caption; 30 | photos: Photo[]; 31 | } 32 | 33 | const photo1 = new Photo(); 34 | photo1.id = 1; 35 | photo1.filename = 'me.jpg'; 36 | 37 | const photo2 = new Photo(); 38 | photo2.id = 2; 39 | photo2.filename = 'she.jpg'; 40 | 41 | const caption = new Caption(); 42 | caption.text = 'cool photo'; 43 | 44 | const user = new User(); 45 | user.caption = caption; 46 | user.firstName = 'Umed Khudoiberdiev'; 47 | user.photos = [photo1, photo2]; 48 | 49 | photo1.user = user; 50 | photo2.user = user; 51 | photo1.users = [user]; 52 | photo2.users = [user]; 53 | 54 | photo1.caption = caption; 55 | photo2.caption = caption; 56 | 57 | const plainUser = instanceToPlain(user, { enableCircularCheck: true }); 58 | expect(plainUser).toEqual({ 59 | firstName: 'Umed Khudoiberdiev', 60 | caption: { text: 'cool photo' }, 61 | photos: [ 62 | { 63 | id: 1, 64 | filename: 'me.jpg', 65 | users: [], 66 | caption: { text: 'cool photo' }, 67 | }, 68 | { 69 | id: 2, 70 | filename: 'she.jpg', 71 | users: [], 72 | caption: { text: 'cool photo' }, 73 | }, 74 | ], 75 | }); 76 | }); 77 | 78 | it('should not skip circular reference objects, but handle it correctly in instanceToInstance operation', () => { 79 | defaultMetadataStorage.clear(); 80 | 81 | class Photo { 82 | id: number; 83 | filename: string; 84 | user: User; 85 | users: User[]; 86 | } 87 | 88 | class User { 89 | id: number; 90 | firstName: string; 91 | photos: Photo[]; 92 | } 93 | 94 | const photo1 = new Photo(); 95 | photo1.id = 1; 96 | photo1.filename = 'me.jpg'; 97 | 98 | const photo2 = new Photo(); 99 | photo2.id = 2; 100 | photo2.filename = 'she.jpg'; 101 | 102 | const user = new User(); 103 | user.firstName = 'Umed Khudoiberdiev'; 104 | user.photos = [photo1, photo2]; 105 | 106 | photo1.user = user; 107 | photo2.user = user; 108 | photo1.users = [user]; 109 | photo2.users = [user]; 110 | 111 | const classUser = instanceToInstance(user, { enableCircularCheck: true }); 112 | expect(classUser).not.toBe(user); 113 | expect(classUser).toBeInstanceOf(User); 114 | expect(classUser).toEqual(user); 115 | }); 116 | 117 | describe('enableCircularCheck option', () => { 118 | class Photo { 119 | id: number; 120 | filename: string; 121 | } 122 | 123 | class User { 124 | id: number; 125 | firstName: string; 126 | photos: Photo[]; 127 | } 128 | let isCircularSpy: jest.SpyInstance; 129 | const photo1 = new Photo(); 130 | photo1.id = 1; 131 | photo1.filename = 'me.jpg'; 132 | 133 | const user = new User(); 134 | user.firstName = 'Umed Khudoiberdiev'; 135 | user.photos = [photo1]; 136 | 137 | beforeEach(() => { 138 | isCircularSpy = jest.spyOn( 139 | TransformOperationExecutor.prototype, 140 | 'isCircular' as any 141 | ); 142 | }); 143 | 144 | afterEach(() => { 145 | isCircularSpy.mockRestore(); 146 | }); 147 | 148 | it('enableCircularCheck option is undefined (default)', () => { 149 | plainToInstance>(User, user); 150 | expect(isCircularSpy).not.toHaveBeenCalled(); 151 | }); 152 | 153 | it('enableCircularCheck option is true', () => { 154 | plainToInstance>(User, user, { 155 | enableCircularCheck: true, 156 | }); 157 | expect(isCircularSpy).toHaveBeenCalled(); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /test/functional/custom-transform.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | import 'reflect-metadata'; 3 | import { Expose, Transform, Type } from '../../src/decorators'; 4 | import { TransformationType } from '../../src/enums'; 5 | import { 6 | ClassTransformOptions, 7 | TransformFnParams, 8 | instanceToInstance, 9 | instanceToPlain, 10 | plainToInstance, 11 | } from '../../src/index'; 12 | import { defaultMetadataStorage } from '../../src/storage'; 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', { 79 | since: 1, 80 | until: 2, 81 | }) 82 | date: Date; 83 | 84 | @Type(() => Date) 85 | @Transform(({ value }) => value.toString(), { groups: ['user'] }) 86 | lastVisitDate: Date; 87 | } 88 | 89 | const plainUser = { 90 | id: 1, 91 | name: 'Johny Cage', 92 | date: new Date().valueOf(), 93 | lastVisitDate: new Date().valueOf(), 94 | }; 95 | 96 | const classedUser1 = plainToInstance(User, plainUser); 97 | expect(classedUser1).toBeInstanceOf(User); 98 | expect(classedUser1.id).toEqual(1); 99 | expect(classedUser1.name).toEqual('Johny Cage'); 100 | expect(classedUser1.date).toBe('custom-transformed-version-check'); 101 | 102 | const classedUser2 = plainToInstance(User, plainUser, { version: 0.5 }); 103 | expect(classedUser2).toBeInstanceOf(User); 104 | expect(classedUser2.id).toEqual(1); 105 | expect(classedUser2.name).toEqual('Johny Cage'); 106 | expect(classedUser2.date).toBeInstanceOf(Date); 107 | 108 | const classedUser3 = plainToInstance(User, plainUser, { version: 1 }); 109 | expect(classedUser3).toBeInstanceOf(User); 110 | expect(classedUser3.id).toEqual(1); 111 | expect(classedUser3.name).toEqual('Johny Cage'); 112 | expect(classedUser3.date).toBe('custom-transformed-version-check'); 113 | 114 | const classedUser4 = plainToInstance(User, plainUser, { version: 2 }); 115 | expect(classedUser4).toBeInstanceOf(User); 116 | expect(classedUser4.id).toEqual(1); 117 | expect(classedUser4.name).toEqual('Johny Cage'); 118 | expect(classedUser4.date).toBeInstanceOf(Date); 119 | 120 | const classedUser5 = plainToInstance(User, plainUser, { groups: ['user'] }); 121 | expect(classedUser5).toBeInstanceOf(User); 122 | expect(classedUser5.id).toEqual(1); 123 | expect(classedUser5.name).toEqual('Johny Cage'); 124 | expect(classedUser5.lastVisitDate).toEqual( 125 | new Date(plainUser.lastVisitDate).toString() 126 | ); 127 | }); 128 | 129 | it('@Transform decorator callback should be given correct arguments', () => { 130 | defaultMetadataStorage.clear(); 131 | 132 | let keyArg: string; 133 | let objArg: any; 134 | let typeArg: TransformationType; 135 | let optionsArg: ClassTransformOptions; 136 | 137 | function transformCallback({ 138 | value, 139 | key, 140 | obj, 141 | type, 142 | options, 143 | }: TransformFnParams): any { 144 | keyArg = key; 145 | objArg = obj; 146 | typeArg = type; 147 | optionsArg = options; 148 | return value; 149 | } 150 | 151 | class User { 152 | @Transform(transformCallback, { toPlainOnly: true }) 153 | @Transform(transformCallback, { toClassOnly: true }) 154 | name: string; 155 | } 156 | 157 | const plainUser = { 158 | name: 'Johny Cage', 159 | }; 160 | const options: ClassTransformOptions = { 161 | groups: ['user', 'user.email'], 162 | version: 2, 163 | }; 164 | 165 | plainToInstance(User, plainUser, options); 166 | expect(keyArg).toBe('name'); 167 | expect(objArg).toEqual(plainUser); 168 | expect(typeArg).toEqual(TransformationType.PLAIN_TO_CLASS); 169 | expect(optionsArg.groups).toBe(options.groups); 170 | expect(optionsArg.version).toBe(options.version); 171 | 172 | const user = new User(); 173 | user.name = 'Johny Cage'; 174 | optionsArg = undefined; 175 | 176 | instanceToPlain(user, options); 177 | expect(keyArg).toBe('name'); 178 | expect(objArg).toEqual(user); 179 | expect(typeArg).toEqual(TransformationType.CLASS_TO_PLAIN); 180 | expect(optionsArg.groups).toBe(options.groups); 181 | expect(optionsArg.version).toBe(options.version); 182 | }); 183 | 184 | let model: any; 185 | it('should serialize json into model instance of class Person', () => { 186 | defaultMetadataStorage.clear(); 187 | expect(() => { 188 | const json = { 189 | name: 'John Doe', 190 | address: { 191 | street: 'Main Street 25', 192 | tel: '5454-534-645', 193 | zip: 10353, 194 | country: 'West Samoa', 195 | }, 196 | age: 25, 197 | hobbies: [ 198 | { type: 'sport', name: 'sailing' }, 199 | { type: 'relax', name: 'reading' }, 200 | { type: 'sport', name: 'jogging' }, 201 | { type: 'relax', name: 'movies' }, 202 | ], 203 | }; 204 | class Hobby { 205 | public type: string; 206 | public name: string; 207 | } 208 | class Address { 209 | public street: string; 210 | 211 | @Expose({ name: 'tel' }) 212 | public telephone: string; 213 | 214 | public zip: number; 215 | 216 | public country: string; 217 | } 218 | class Person { 219 | public name: string; 220 | 221 | @Type(() => Address) 222 | public address: Address; 223 | 224 | @Type(() => Hobby) 225 | @Transform( 226 | ({ value }) => value.filter((hobby: any) => hobby.type === 'sport'), 227 | { toClassOnly: true } 228 | ) 229 | public hobbies: Hobby[]; 230 | 231 | public age: number; 232 | } 233 | model = plainToInstance(Person, json); 234 | expect(model instanceof Person); 235 | expect(model.address instanceof Address); 236 | model.hobbies.forEach((hobby: Hobby) => 237 | expect(hobby instanceof Hobby && hobby.type === 'sport') 238 | ); 239 | }).not.toThrow(); 240 | }); 241 | 242 | it('should serialize json into model instance of class Person with different possibilities for type of one property (polymorphism)', () => { 243 | defaultMetadataStorage.clear(); 244 | expect(() => { 245 | const json = { 246 | name: 'John Doe', 247 | hobby: { 248 | __type: 'program', 249 | name: 'typescript coding', 250 | specialAbility: 'testing', 251 | }, 252 | }; 253 | 254 | abstract class Hobby { 255 | public name: string; 256 | } 257 | 258 | class Sports extends Hobby { 259 | // Empty 260 | } 261 | 262 | class Relaxing extends Hobby { 263 | // Empty 264 | } 265 | 266 | class Programming extends Hobby { 267 | @Transform(({ value }) => value.toUpperCase()) 268 | specialAbility: string; 269 | } 270 | 271 | class Person { 272 | public name: string; 273 | 274 | @Type(() => Hobby, { 275 | discriminator: { 276 | property: '__type', 277 | subTypes: [ 278 | { value: Sports, name: 'sports' }, 279 | { value: Relaxing, name: 'relax' }, 280 | { value: Programming, name: 'program' }, 281 | ], 282 | }, 283 | }) 284 | public hobby: any; 285 | } 286 | 287 | const expectedHobby = { 288 | name: 'typescript coding', 289 | specialAbility: 'TESTING', 290 | }; 291 | 292 | const model: Person = plainToInstance(Person, json); 293 | expect(model).toBeInstanceOf(Person); 294 | expect(model.hobby).toBeInstanceOf(Programming); 295 | expect(model.hobby).not.toHaveProperty('__type'); 296 | expect(model.hobby).toHaveProperty('specialAbility', 'TESTING'); 297 | }).not.toThrow(); 298 | }); 299 | 300 | it('should serialize json into model instance of class Person with different types in array (polymorphism)', () => { 301 | defaultMetadataStorage.clear(); 302 | expect(() => { 303 | const json = { 304 | name: 'John Doe', 305 | hobbies: [ 306 | { 307 | __type: 'program', 308 | name: 'typescript coding', 309 | specialAbility: 'testing', 310 | }, 311 | { __type: 'relax', name: 'sun' }, 312 | ], 313 | }; 314 | 315 | abstract class Hobby { 316 | public name: string; 317 | } 318 | 319 | class Sports extends Hobby { 320 | // Empty 321 | } 322 | 323 | class Relaxing extends Hobby { 324 | // Empty 325 | } 326 | 327 | class Programming extends Hobby { 328 | @Transform(({ value }) => value.toUpperCase()) 329 | specialAbility: string; 330 | } 331 | 332 | class Person { 333 | public name: string; 334 | 335 | @Type(() => Hobby, { 336 | discriminator: { 337 | property: '__type', 338 | subTypes: [ 339 | { value: Sports, name: 'sports' }, 340 | { value: Relaxing, name: 'relax' }, 341 | { value: Programming, name: 'program' }, 342 | ], 343 | }, 344 | }) 345 | public hobbies: any[]; 346 | } 347 | 348 | const model: Person = plainToInstance(Person, json); 349 | expect(model).toBeInstanceOf(Person); 350 | expect(model.hobbies[0]).toBeInstanceOf(Programming); 351 | expect(model.hobbies[1]).toBeInstanceOf(Relaxing); 352 | expect(model.hobbies[0]).not.toHaveProperty('__type'); 353 | expect(model.hobbies[1]).not.toHaveProperty('__type'); 354 | expect(model.hobbies[1]).toHaveProperty('name', 'sun'); 355 | expect(model.hobbies[0]).toHaveProperty('specialAbility', 'TESTING'); 356 | }).not.toThrow(); 357 | }); 358 | 359 | it('should serialize json into model instance of class Person with different possibilities for type of one property AND keeps discriminator property (polymorphism)', () => { 360 | defaultMetadataStorage.clear(); 361 | expect(() => { 362 | const json = { 363 | name: 'John Doe', 364 | hobby: { 365 | __type: 'program', 366 | name: 'typescript coding', 367 | specialAbility: 'testing', 368 | }, 369 | }; 370 | 371 | abstract class Hobby { 372 | public name: string; 373 | } 374 | 375 | class Sports extends Hobby { 376 | // Empty 377 | } 378 | 379 | class Relaxing extends Hobby { 380 | // Empty 381 | } 382 | 383 | class Programming extends Hobby { 384 | @Transform(({ value }) => value.toUpperCase()) 385 | specialAbility: string; 386 | } 387 | 388 | class Person { 389 | public name: string; 390 | 391 | @Type(() => Hobby, { 392 | discriminator: { 393 | property: '__type', 394 | subTypes: [ 395 | { value: Sports, name: 'sports' }, 396 | { value: Relaxing, name: 'relax' }, 397 | { value: Programming, name: 'program' }, 398 | ], 399 | }, 400 | keepDiscriminatorProperty: true, 401 | }) 402 | public hobby: any; 403 | } 404 | 405 | const model: Person = plainToInstance(Person, json); 406 | expect(model).toBeInstanceOf(Person); 407 | expect(model.hobby).toBeInstanceOf(Programming); 408 | expect(model.hobby).toHaveProperty('__type'); 409 | expect(model.hobby).toHaveProperty('specialAbility', 'TESTING'); 410 | }).not.toThrow(); 411 | }); 412 | 413 | it('should serialize json into model instance of class Person with different types in array AND keeps discriminator property (polymorphism)', () => { 414 | defaultMetadataStorage.clear(); 415 | expect(() => { 416 | const json = { 417 | name: 'John Doe', 418 | hobbies: [ 419 | { 420 | __type: 'program', 421 | name: 'typescript coding', 422 | specialAbility: 'testing', 423 | }, 424 | { __type: 'relax', name: 'sun' }, 425 | ], 426 | }; 427 | 428 | abstract class Hobby { 429 | public name: string; 430 | } 431 | 432 | class Sports extends Hobby { 433 | // Empty 434 | } 435 | 436 | class Relaxing extends Hobby { 437 | // Empty 438 | } 439 | 440 | class Programming extends Hobby { 441 | @Transform(({ value }) => value.toUpperCase()) 442 | specialAbility: string; 443 | } 444 | 445 | class Person { 446 | public name: string; 447 | 448 | @Type(() => Hobby, { 449 | discriminator: { 450 | property: '__type', 451 | subTypes: [ 452 | { value: Sports, name: 'sports' }, 453 | { value: Relaxing, name: 'relax' }, 454 | { value: Programming, name: 'program' }, 455 | ], 456 | }, 457 | keepDiscriminatorProperty: true, 458 | }) 459 | public hobbies: any[]; 460 | } 461 | 462 | const model: Person = plainToInstance(Person, json); 463 | expect(model).toBeInstanceOf(Person); 464 | expect(model.hobbies[0]).toBeInstanceOf(Programming); 465 | expect(model.hobbies[1]).toBeInstanceOf(Relaxing); 466 | expect(model.hobbies[0]).toHaveProperty('__type'); 467 | expect(model.hobbies[1]).toHaveProperty('__type'); 468 | expect(model.hobbies[1]).toHaveProperty('name', 'sun'); 469 | expect(model.hobbies[0]).toHaveProperty('specialAbility', 'TESTING'); 470 | }).not.toThrow(); 471 | }); 472 | 473 | it('should deserialize class Person into json with different possibilities for type of one property (polymorphism)', () => { 474 | defaultMetadataStorage.clear(); 475 | expect(() => { 476 | abstract class Hobby { 477 | public name: string; 478 | } 479 | 480 | class Sports extends Hobby { 481 | // Empty 482 | } 483 | 484 | class Relaxing extends Hobby { 485 | // Empty 486 | } 487 | 488 | class Programming extends Hobby { 489 | @Transform(({ value }) => value.toUpperCase()) 490 | specialAbility: string; 491 | } 492 | 493 | class Person { 494 | public name: string; 495 | 496 | @Type(() => Hobby, { 497 | discriminator: { 498 | property: '__type', 499 | subTypes: [ 500 | { value: Sports, name: 'sports' }, 501 | { value: Relaxing, name: 'relax' }, 502 | { value: Programming, name: 'program' }, 503 | ], 504 | }, 505 | }) 506 | public hobby: any; 507 | } 508 | 509 | const model: Person = new Person(); 510 | const program = new Programming(); 511 | program.name = 'typescript coding'; 512 | program.specialAbility = 'testing'; 513 | model.name = 'John Doe'; 514 | model.hobby = program; 515 | const json: any = instanceToPlain(model); 516 | expect(json).not.toBeInstanceOf(Person); 517 | expect(json.hobby).toHaveProperty('__type', 'program'); 518 | }).not.toThrow(); 519 | }); 520 | 521 | it('should deserialize class Person into json with different types in array (polymorphism)', () => { 522 | defaultMetadataStorage.clear(); 523 | expect(() => { 524 | abstract class Hobby { 525 | public name: string; 526 | } 527 | 528 | class Sports extends Hobby { 529 | // Empty 530 | } 531 | 532 | class Relaxing extends Hobby { 533 | // Empty 534 | } 535 | 536 | class Programming extends Hobby { 537 | @Transform(({ value }) => value.toUpperCase()) 538 | specialAbility: string; 539 | } 540 | 541 | class Person { 542 | public name: string; 543 | 544 | @Type(() => Hobby, { 545 | discriminator: { 546 | property: '__type', 547 | subTypes: [ 548 | { value: Sports, name: 'sports' }, 549 | { value: Relaxing, name: 'relax' }, 550 | { value: Programming, name: 'program' }, 551 | ], 552 | }, 553 | }) 554 | public hobbies: any[]; 555 | } 556 | 557 | const model: Person = new Person(); 558 | const sport = new Sports(); 559 | sport.name = 'Football'; 560 | const program = new Programming(); 561 | program.name = 'typescript coding'; 562 | program.specialAbility = 'testing'; 563 | model.name = 'John Doe'; 564 | model.hobbies = [sport, program]; 565 | const json: any = instanceToPlain(model); 566 | expect(json).not.toBeInstanceOf(Person); 567 | expect(json.hobbies[0]).toHaveProperty('__type', 'sports'); 568 | expect(json.hobbies[1]).toHaveProperty('__type', 'program'); 569 | }).not.toThrow(); 570 | }); 571 | 572 | /** 573 | * test-case for issue #520 574 | */ 575 | it('should deserialize undefined union type to undefined', () => { 576 | defaultMetadataStorage.clear(); 577 | expect(() => { 578 | abstract class Hobby { 579 | public name: string; 580 | } 581 | 582 | class Sports extends Hobby { 583 | // Empty 584 | } 585 | 586 | class Relaxing extends Hobby { 587 | // Empty 588 | } 589 | 590 | class Programming extends Hobby { 591 | @Transform(({ value }) => value.toUpperCase()) 592 | specialAbility: string; 593 | } 594 | 595 | class Person { 596 | public name: string; 597 | 598 | @Type(() => Hobby, { 599 | discriminator: { 600 | property: '__type', 601 | subTypes: [ 602 | { value: Sports, name: 'sports' }, 603 | { value: Relaxing, name: 'relax' }, 604 | { value: Programming, name: 'program' }, 605 | ], 606 | }, 607 | }) 608 | public hobby: Hobby; 609 | } 610 | 611 | const model: Person = new Person(); 612 | const sport = new Sports(); 613 | sport.name = 'Football'; 614 | const program = new Programming(); 615 | program.name = 'typescript coding'; 616 | program.specialAbility = 'testing'; 617 | model.name = 'John Doe'; 618 | // NOTE: hobby remains undefined 619 | model.hobby = undefined; 620 | const json: any = instanceToPlain(model); 621 | expect(json).not.toBeInstanceOf(Person); 622 | expect(json.hobby).toBeUndefined(); 623 | }).not.toThrow(); 624 | }); 625 | 626 | it('should transform class Person into class OtherPerson with different possibilities for type of one property (polymorphism)', () => { 627 | defaultMetadataStorage.clear(); 628 | expect(() => { 629 | abstract class Hobby { 630 | public name: string; 631 | } 632 | 633 | class Sports extends Hobby { 634 | // Empty 635 | } 636 | 637 | class Relaxing extends Hobby { 638 | // Empty 639 | } 640 | 641 | class Programming extends Hobby { 642 | @Transform(({ value }) => value.toUpperCase()) 643 | specialAbility: string; 644 | } 645 | 646 | class Person { 647 | public name: string; 648 | 649 | @Type(() => Hobby, { 650 | discriminator: { 651 | property: '__type', 652 | subTypes: [ 653 | { value: Sports, name: 'sports' }, 654 | { value: Relaxing, name: 'relax' }, 655 | { value: Programming, name: 'program' }, 656 | ], 657 | }, 658 | }) 659 | public hobby: any; 660 | } 661 | 662 | const model: Person = new Person(); 663 | const program = new Programming(); 664 | program.name = 'typescript coding'; 665 | program.specialAbility = 'testing'; 666 | model.name = 'John Doe'; 667 | model.hobby = program; 668 | const person: Person = instanceToInstance(model); 669 | expect(person).toBeInstanceOf(Person); 670 | expect(person.hobby).not.toHaveProperty('__type'); 671 | }).not.toThrow(); 672 | }); 673 | 674 | it('should transform class Person into class OtherPerson with different types in array (polymorphism)', () => { 675 | defaultMetadataStorage.clear(); 676 | expect(() => { 677 | abstract class Hobby { 678 | public name: string; 679 | } 680 | 681 | class Sports extends Hobby { 682 | // Empty 683 | } 684 | 685 | class Relaxing extends Hobby { 686 | // Empty 687 | } 688 | 689 | class Programming extends Hobby { 690 | @Transform(({ value }) => value.toUpperCase()) 691 | specialAbility: string; 692 | } 693 | 694 | class Person { 695 | public name: string; 696 | 697 | @Type(() => Hobby, { 698 | discriminator: { 699 | property: '__type', 700 | subTypes: [ 701 | { value: Sports, name: 'sports' }, 702 | { value: Relaxing, name: 'relax' }, 703 | { value: Programming, name: 'program' }, 704 | ], 705 | }, 706 | }) 707 | public hobbies: any[]; 708 | } 709 | 710 | const model: Person = new Person(); 711 | const sport = new Sports(); 712 | sport.name = 'Football'; 713 | const program = new Programming(); 714 | program.name = 'typescript coding'; 715 | program.specialAbility = 'testing'; 716 | model.name = 'John Doe'; 717 | model.hobbies = [sport, program]; 718 | const person: Person = instanceToInstance(model); 719 | expect(person).toBeInstanceOf(Person); 720 | expect(person.hobbies[0]).not.toHaveProperty('__type'); 721 | expect(person.hobbies[1]).not.toHaveProperty('__type'); 722 | }).not.toThrow(); 723 | }); 724 | 725 | it('should serialize json into model instance of class Person with different possibilities for type of one property AND uses default as fallback (polymorphism)', () => { 726 | defaultMetadataStorage.clear(); 727 | expect(() => { 728 | const json = { 729 | name: 'John Doe', 730 | hobby: { 731 | __type: 'program', 732 | name: 'typescript coding', 733 | specialAbility: 'testing', 734 | }, 735 | }; 736 | 737 | abstract class Hobby { 738 | public name: string; 739 | } 740 | 741 | class Sports extends Hobby { 742 | // Empty 743 | } 744 | 745 | class Relaxing extends Hobby { 746 | // Empty 747 | } 748 | 749 | class Programming extends Hobby { 750 | @Transform(({ value }) => value.toUpperCase()) 751 | specialAbility: string; 752 | } 753 | 754 | class Person { 755 | public name: string; 756 | 757 | @Type(() => Hobby, { 758 | discriminator: { 759 | property: '__type', 760 | subTypes: [], 761 | }, 762 | }) 763 | public hobby: any; 764 | } 765 | 766 | const model: Person = plainToInstance(Person, json); 767 | expect(model).toBeInstanceOf(Person); 768 | expect(model.hobby).toBeInstanceOf(Hobby); 769 | expect(model.hobby).not.toHaveProperty('__type'); 770 | expect(model.hobby).toHaveProperty('specialAbility', 'testing'); 771 | }).not.toThrow(); 772 | }); 773 | 774 | it('should serialize json into model instance of class Person with different types in array AND uses default as fallback (polymorphism)', () => { 775 | defaultMetadataStorage.clear(); 776 | expect(() => { 777 | const json = { 778 | name: 'John Doe', 779 | hobbies: [ 780 | { 781 | __type: 'program', 782 | name: 'typescript coding', 783 | specialAbility: 'testing', 784 | }, 785 | { __type: 'relax', name: 'sun' }, 786 | ], 787 | }; 788 | 789 | abstract class Hobby { 790 | public name: string; 791 | } 792 | 793 | class Sports extends Hobby { 794 | // Empty 795 | } 796 | 797 | class Relaxing extends Hobby { 798 | // Empty 799 | } 800 | 801 | class Programming extends Hobby { 802 | @Transform(({ value }) => value.toUpperCase()) 803 | specialAbility: string; 804 | } 805 | 806 | class Person { 807 | public name: string; 808 | 809 | @Type(() => Hobby, { 810 | discriminator: { 811 | property: '__type', 812 | subTypes: [], 813 | }, 814 | }) 815 | public hobbies: any[]; 816 | } 817 | 818 | const model: Person = plainToInstance(Person, json); 819 | expect(model).toBeInstanceOf(Person); 820 | expect(model.hobbies[0]).toBeInstanceOf(Hobby); 821 | expect(model.hobbies[1]).toBeInstanceOf(Hobby); 822 | expect(model.hobbies[0]).not.toHaveProperty('__type'); 823 | expect(model.hobbies[1]).not.toHaveProperty('__type'); 824 | expect(model.hobbies[1]).toHaveProperty('name', 'sun'); 825 | expect(model.hobbies[0]).toHaveProperty('specialAbility', 'testing'); 826 | }).not.toThrow(); 827 | }); 828 | 829 | it('should serialize a model into json', () => { 830 | expect(() => { 831 | instanceToPlain(model); 832 | }).not.toThrow(); 833 | }); 834 | }); 835 | -------------------------------------------------------------------------------- /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, { 33 | exposeDefaultValues: true, 34 | }); 35 | 36 | expect(transformedUser).toBeInstanceOf(User); 37 | expect(transformedUser).toEqual({ 38 | age: undefined, 39 | ageWithDefault: 18, 40 | firstName: undefined, 41 | firstNameWithDefault: 'default first name', 42 | adminWithDefault: false, 43 | lastNameWithDefault: 'default last name', 44 | }); 45 | }); 46 | 47 | it('should take exposed values and ignore defaults', () => { 48 | const fromPlainUser = {}; 49 | const transformedUser = plainToInstance(User, fromPlainUser); 50 | 51 | expect(transformedUser).toBeInstanceOf(User); 52 | expect(transformedUser).toEqual({ 53 | age: NaN, 54 | ageWithDefault: NaN, 55 | firstName: undefined, 56 | firstNameWithDefault: undefined, 57 | adminWithDefault: false, 58 | lastNameWithDefault: 'default last name', 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/functional/es6-data-types.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Type } from '../../src/decorators'; 3 | import { Expose, instanceToPlain, plainToInstance } from '../../src/index'; 4 | import { defaultMetadataStorage } from '../../src/storage'; 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, { 344 | excludeExtraneousValues: false, 345 | }); 346 | checkPlainToClassUser(classedUser); 347 | 348 | const classedUser2 = plainToInstance(User, plainUser, { 349 | excludeExtraneousValues: true, 350 | }); 351 | checkPlainToClassUser(classedUser2); 352 | }); 353 | }); 354 | -------------------------------------------------------------------------------- /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({ 37 | implicitTypeNumber: 100, 38 | implicitTypeString: '133123', 39 | }); 40 | expect(result2).toEqual({ 41 | implicitTypeNumber: '100', 42 | implicitTypeString: 133123, 43 | }); 44 | }); 45 | }); 46 | 47 | describe('implicit and explicity type declarations', () => { 48 | defaultMetadataStorage.clear(); 49 | 50 | class Example { 51 | @Expose() 52 | readonly implicitTypeViaOtherDecorator: Date; 53 | 54 | @Type() 55 | readonly implicitTypeViaEmptyTypeDecorator: number; 56 | 57 | @Type(() => String) 58 | readonly explicitType: string; 59 | } 60 | 61 | const result: Example = plainToInstance( 62 | Example, 63 | { 64 | implicitTypeViaOtherDecorator: '2018-12-24T12:00:00Z', 65 | implicitTypeViaEmptyTypeDecorator: '100', 66 | explicitType: 100, 67 | }, 68 | { enableImplicitConversion: true } 69 | ); 70 | 71 | it('should use implicitly defined design:type to convert value when no @Type decorator is used', () => { 72 | expect(result.implicitTypeViaOtherDecorator).toBeInstanceOf(Date); 73 | expect(result.implicitTypeViaOtherDecorator.getTime()).toEqual( 74 | new Date('2018-12-24T12:00:00Z').getTime() 75 | ); 76 | }); 77 | 78 | it('should use implicitly defined design:type to convert value when empty @Type() decorator is used', () => { 79 | expect(typeof result.implicitTypeViaEmptyTypeDecorator).toBe('number'); 80 | expect(result.implicitTypeViaEmptyTypeDecorator).toEqual(100); 81 | }); 82 | 83 | it('should use explicitly defined type when @Type(() => Construtable) decorator is used', () => { 84 | expect(typeof result.explicitType).toBe('string'); 85 | expect(result.explicitType).toEqual('100'); 86 | }); 87 | }); 88 | 89 | describe('plainToInstance transforms built-in primitive types properly', () => { 90 | defaultMetadataStorage.clear(); 91 | 92 | class Example { 93 | @Type() 94 | date: Date; 95 | 96 | @Type() 97 | string: string; 98 | 99 | @Type() 100 | string2: string; 101 | 102 | @Type() 103 | number: number; 104 | 105 | @Type() 106 | number2: number; 107 | 108 | @Type() 109 | boolean: boolean; 110 | 111 | @Type() 112 | boolean2: boolean; 113 | } 114 | 115 | const result: Example = plainToInstance( 116 | Example, 117 | { 118 | date: '2018-12-24T12:00:00Z', 119 | string: '100', 120 | string2: 100, 121 | number: '100', 122 | number2: 100, 123 | boolean: 1, 124 | boolean2: 0, 125 | }, 126 | { enableImplicitConversion: true } 127 | ); 128 | 129 | it('should recognize and convert to Date', () => { 130 | expect(result.date).toBeInstanceOf(Date); 131 | expect(result.date.getTime()).toEqual( 132 | new Date('2018-12-24T12:00:00Z').getTime() 133 | ); 134 | }); 135 | 136 | it('should recognize and convert to string', () => { 137 | expect(typeof result.string).toBe('string'); 138 | expect(typeof result.string2).toBe('string'); 139 | expect(result.string).toEqual('100'); 140 | expect(result.string2).toEqual('100'); 141 | }); 142 | 143 | it('should recognize and convert to number', () => { 144 | expect(typeof result.number).toBe('number'); 145 | expect(typeof result.number2).toBe('number'); 146 | expect(result.number).toEqual(100); 147 | expect(result.number2).toEqual(100); 148 | }); 149 | 150 | it('should recognize and convert to boolean', () => { 151 | expect(result.boolean).toBeTruthy(); 152 | expect(result.boolean2).toBeFalsy(); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /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( 39 | new Date(1967, 2, 1).getTime() 40 | ); 41 | expect(classedStudent.id).toEqual(plainStudent.id); 42 | expect(classedStudent.email).toEqual(plainStudent.email); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /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 | {} 179 | ); 180 | expect(instanceToPlain(new User(), { exposeUnsetFields: true })).toEqual({ 181 | firstName: undefined, 182 | lastName: undefined, 183 | }); 184 | 185 | const classedUser = plainToInstance(User, { exposeUnsetFields: false }); 186 | expect(classedUser).toBeInstanceOf(User); 187 | expect(classedUser).toEqual({ 188 | firstName: undefined, 189 | lastName: undefined, 190 | }); 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------