Skip to content

Commit e6e8b82

Browse files
merge: release 0.2.0
2 parents d61a97a + 3ba33d8 commit e6e8b82

13 files changed

+852
-74
lines changed

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ language: node_js
22
node_js:
33
- stable
44
- 8
5-
- 6
65

76
after_success:
87
- bash <(curl -s https://codecov.io/bash)

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog and release notes
22

3+
### 0.2.0 [BREAKING CHANGE]
4+
5+
#### Added
6+
7+
- add documentation for using `Set`s and `Map`s
8+
- add opotion to pass a discriminator function to convert values into different types based on custom conditions
9+
- added support for polymorphism based on a named type property
10+
11+
#### Fixed
12+
13+
- fix bug when transforming `null` values as primitives
14+
315
### 0.1.10
416

517
#### Fixed

README.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,78 @@ let album = plainToClass(Album, albumJson);
267267
// now album is Album object with Photo objects inside
268268
```
269269

270+
### Providing more than one type option
271+
272+
In case the nested object can be of different types, you can provide an additional options object,
273+
that specifies a discriminator. The discriminator option must define a `property` that holds the sub
274+
type name for the object and the possible `subTypes`, the nested object can converted to. A sub type
275+
has a `value`, that holds the constructor of the Type and the `name`, that can match with the `property`
276+
of the discriminator.
277+
278+
Lets say we have an album that has a top photo. But this photo can be of certain different types.
279+
And we are trying to convert album plain object to class object. The plain object input has to define
280+
the additional property `__type`. This property is removed during transformation by default:
281+
282+
**JSON input**:
283+
```json
284+
{
285+
"id": 1,
286+
"name": "foo",
287+
"topPhoto": {
288+
"id": 9,
289+
"filename": "cool_wale.jpg",
290+
"depth": 1245,
291+
"__type": "underwater"
292+
}
293+
}
294+
```
295+
296+
```typescript
297+
import {Type, plainToClass} from "class-transformer";
298+
299+
export abstract class Photo {
300+
id: number;
301+
filename: string;
302+
}
303+
304+
export class Landscape extends Photo {
305+
panorama: boolean;
306+
}
307+
308+
export class Portrait extends Photo {
309+
person: Person;
310+
}
311+
312+
export class UnderWater extends Photo {
313+
depth: number;
314+
}
315+
316+
export class Album {
317+
318+
id: number;
319+
name: string;
320+
321+
@Type(() => Photo, {
322+
discriminator: {
323+
property: "__type",
324+
subTypes: [
325+
{ value: Landscape, name: "landscape" },
326+
{ value: Portrait, name: "portrait" },
327+
{ value: UnderWater, name: "underwater" }
328+
]
329+
}
330+
})
331+
topPhoto: Landscape | Portrait | UnderWater;
332+
333+
}
334+
335+
let album = plainToClass(Album, albumJson);
336+
// now album is Album object with a UnderWater object without `__type` property.
337+
```
338+
339+
Hint: The same applies for arrays with different sub types. Moreover you can specify `keepDiscriminatorProperty: true`
340+
in the options to keep the discriminator property also inside your resulting class.
341+
270342
## Exposing getters and method return values
271343

272344
You can expose what your getter or method return by setting a `@Expose()` decorator to those getters or methods:
@@ -565,6 +637,29 @@ export class Photo {
565637

566638
Library will handle proper transformation automatically.
567639

640+
ES6 collections `Set` and `Map` also require the `@Type` decorator:
641+
642+
```typescript
643+
export class Skill {
644+
name: string;
645+
}
646+
647+
export class Weapon {
648+
name: string;
649+
range: number;
650+
}
651+
652+
export class Player {
653+
name: string;
654+
655+
@Type(() => Skill)
656+
skills: Set<Skill>;
657+
658+
@Type(() => Weapon)
659+
weapons: Map<string, Weapon>;
660+
}
661+
```
662+
568663
## Additional data transformation
569664

570665
### Basic usage
@@ -611,6 +706,7 @@ The `@Transform` decorator is given more arguments to let you configure how you
611706
|--------------------|------------------------------------------|---------------------------------------------|
612707
| `@TransformClassToPlain` | `@TransformClassToPlain({ groups: ["user"] })` | Transform the method return with classToPlain and expose the properties on the class.
613708
| `@TransformClassToClass` | `@TransformClassToClass({ groups: ["user"] })` | Transform the method return with classToClass and expose the properties on the class.
709+
| `@TransformPlainToClass` | `@TransformPlainToClass(User, { groups: ["user"] })` | Transform the method return with plainToClass and expose the properties on the class.
614710

615711
The above decorators accept one optional argument:
616712
ClassTransformOptions - The transform options like groups, version, name

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "class-transformer",
3-
"version": "0.1.10",
3+
"version": "0.2.0",
44
"description": "Proper decorator-based transformation / serialization / deserialization of plain javascript objects to class constructors",
55
"license": "MIT",
66
"readmeFilename": "README.md",

src/TransformOperationExecutor.ts

Lines changed: 96 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import {ClassTransformOptions} from "./ClassTransformOptions";
2-
import {defaultMetadataStorage} from "./storage";
3-
import {TypeOptions} from "./metadata/ExposeExcludeOptions";
1+
import { ClassTransformOptions } from "./ClassTransformOptions";
2+
import { defaultMetadataStorage } from "./storage";
3+
import { TypeHelpOptions, Discriminator, TypeOptions } from "./metadata/ExposeExcludeOptions";
4+
import { TypeMetadata } from "./metadata/TypeMetadata";
45

56
export enum TransformationType {
67
PLAIN_TO_CLASS,
@@ -14,33 +15,52 @@ export class TransformOperationExecutor {
1415
// Private Properties
1516
// -------------------------------------------------------------------------
1617

17-
private transformedTypesMap = new Map<Object, {level: number, object: Object}>();
18+
private recursionStack = new Set<Object>();
1819

1920
// -------------------------------------------------------------------------
2021
// Constructor
2122
// -------------------------------------------------------------------------
2223

2324
constructor(private transformationType: TransformationType,
24-
private options: ClassTransformOptions) {
25+
private options: ClassTransformOptions) {
2526
}
2627

2728
// -------------------------------------------------------------------------
2829
// Public Methods
2930
// -------------------------------------------------------------------------
3031

31-
transform(source: Object|Object[]|any,
32-
value: Object|Object[]|any,
33-
targetType: Function,
34-
arrayType: Function,
35-
isMap: boolean,
36-
level: number = 0) {
32+
transform(source: Object | Object[] | any,
33+
value: Object | Object[] | any,
34+
targetType: Function | TypeMetadata,
35+
arrayType: Function,
36+
isMap: boolean,
37+
level: number = 0) {
3738

38-
if (value instanceof Array || value instanceof Set) {
39+
if (Array.isArray(value) || value instanceof Set) {
3940
const newValue = arrayType && this.transformationType === TransformationType.PLAIN_TO_CLASS ? new (arrayType as any)() : [];
4041
(value as any[]).forEach((subValue, index) => {
4142
const subSource = source ? source[index] : undefined;
42-
if (!this.options.enableCircularCheck || !this.isCircular(subValue, level)) {
43-
const value = this.transform(subSource, subValue, targetType, undefined, subValue instanceof Map, level + 1);
43+
if (!this.options.enableCircularCheck || !this.isCircular(subValue)) {
44+
let realTargetType;
45+
if (typeof targetType !== "function" && targetType && targetType.options && targetType.options.discriminator && targetType.options.discriminator.property && targetType.options.discriminator.subTypes) {
46+
if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
47+
realTargetType = targetType.options.discriminator.subTypes.find((subType) => subType.name === subValue[(targetType as { options: TypeOptions }).options.discriminator.property]);
48+
const options: TypeHelpOptions = { newObject: newValue, object: subValue, property: undefined };
49+
const newType = targetType.typeFunction(options);
50+
realTargetType === undefined ? realTargetType = newType : realTargetType = realTargetType.value;
51+
if (!targetType.options.keepDiscriminatorProperty) delete subValue[targetType.options.discriminator.property];
52+
}
53+
if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
54+
realTargetType = subValue.constructor;
55+
}
56+
if (this.transformationType === TransformationType.CLASS_TO_PLAIN) {
57+
subValue[targetType.options.discriminator.property] = targetType.options.discriminator.subTypes.find((subType) => subType.value === subValue.constructor).name;
58+
}
59+
} else {
60+
realTargetType = targetType;
61+
}
62+
const value = this.transform(subSource, subValue, realTargetType, undefined, subValue instanceof Map, level + 1);
63+
4464
if (newValue instanceof Set) {
4565
newValue.add(value);
4666
} else {
@@ -55,14 +75,19 @@ export class TransformOperationExecutor {
5575
}
5676
});
5777
return newValue;
58-
5978
} else if (targetType === String && !isMap) {
79+
if (value === null || value === undefined)
80+
return value;
6081
return String(value);
6182

6283
} else if (targetType === Number && !isMap) {
84+
if (value === null || value === undefined)
85+
return value;
6386
return Number(value);
6487

6588
} else if (targetType === Boolean && !isMap) {
89+
if (value === null || value === undefined)
90+
return value;
6691
return Boolean(value);
6792

6893
} else if ((targetType === Date || value instanceof Date) && !isMap) {
@@ -82,10 +107,10 @@ export class TransformOperationExecutor {
82107

83108
if (this.options.enableCircularCheck) {
84109
// add transformed type to prevent circular references
85-
this.transformedTypesMap.set(value, {level: level, object: value});
110+
this.recursionStack.add(value);
86111
}
87112

88-
const keys = this.getKeys(targetType, value);
113+
const keys = this.getKeys((targetType as Function), value);
89114
let newValue: any = source ? source : {};
90115
if (!source && (this.transformationType === TransformationType.PLAIN_TO_CLASS || this.transformationType === TransformationType.CLASS_TO_CLASS)) {
91116
if (isMap) {
@@ -103,16 +128,17 @@ export class TransformOperationExecutor {
103128
let valueKey = key, newValueKey = key, propertyName = key;
104129
if (!this.options.ignoreDecorators && targetType) {
105130
if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
106-
const exposeMetadata = defaultMetadataStorage.findExposeMetadataByCustomName(targetType, key);
131+
const exposeMetadata = defaultMetadataStorage.findExposeMetadataByCustomName((targetType as Function), key);
107132
if (exposeMetadata) {
108133
propertyName = exposeMetadata.propertyName;
109134
newValueKey = exposeMetadata.propertyName;
110135
}
111136

112137
} else if (this.transformationType === TransformationType.CLASS_TO_PLAIN || this.transformationType === TransformationType.CLASS_TO_CLASS) {
113-
const exposeMetadata = defaultMetadataStorage.findExposeMetadata(targetType, key);
114-
if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name)
138+
const exposeMetadata = defaultMetadataStorage.findExposeMetadata((targetType as Function), key);
139+
if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name) {
115140
newValueKey = exposeMetadata.options.name;
141+
}
116142
}
117143
}
118144

@@ -132,10 +158,30 @@ export class TransformOperationExecutor {
132158
type = targetType;
133159

134160
} else if (targetType) {
135-
const metadata = defaultMetadataStorage.findTypeMetadata(targetType, propertyName);
161+
162+
const metadata = defaultMetadataStorage.findTypeMetadata((targetType as Function), propertyName);
136163
if (metadata) {
137-
const options: TypeOptions = {newObject: newValue, object: value, property: propertyName};
138-
type = metadata.typeFunction(options);
164+
const options: TypeHelpOptions = { newObject: newValue, object: value, property: propertyName };
165+
const newType = metadata.typeFunction(options);
166+
if (metadata.options && metadata.options.discriminator && metadata.options.discriminator.property && metadata.options.discriminator.subTypes) {
167+
if (!(value[valueKey] instanceof Array)) {
168+
if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
169+
type = metadata.options.discriminator.subTypes.find((subType) => subType.name === subValue[metadata.options.discriminator.property]);
170+
type === undefined ? type = newType : type = type.value;
171+
if (!metadata.options.keepDiscriminatorProperty) delete subValue[metadata.options.discriminator.property];
172+
}
173+
if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
174+
type = subValue.constructor;
175+
}
176+
if (this.transformationType === TransformationType.CLASS_TO_PLAIN) {
177+
subValue[metadata.options.discriminator.property] = metadata.options.discriminator.subTypes.find((subType) => subType.value === subValue.constructor).name;
178+
}
179+
} else {
180+
type = metadata;
181+
}
182+
} else {
183+
type = newType;
184+
}
139185
isSubValueMap = isSubValueMap || metadata.reflectedType === Map;
140186
} else if (this.options.targetMaps) { // try to find a type in target maps
141187
this.options.targetMaps
@@ -145,7 +191,8 @@ export class TransformOperationExecutor {
145191
}
146192

147193
// if value is an array try to get its custom array type
148-
const arrayType = value[valueKey] instanceof Array ? this.getReflectedType(targetType, propertyName) : undefined;
194+
const arrayType = Array.isArray(value[valueKey]) ? this.getReflectedType((targetType as Function), propertyName) : undefined;
195+
149196
// const subValueKey = TransformationType === TransformationType.PLAIN_TO_CLASS && newKeyName ? newKeyName : key;
150197
const subSource = source ? source[valueKey] : undefined;
151198

@@ -162,18 +209,32 @@ export class TransformOperationExecutor {
162209
continue;
163210
}
164211

165-
if (!this.options.enableCircularCheck || !this.isCircular(subValue, level)) {
212+
if (!this.options.enableCircularCheck || !this.isCircular(subValue)) {
166213
let transformKey = this.transformationType === TransformationType.PLAIN_TO_CLASS ? newValueKey : key;
167-
let finalValue = this.transform(subSource, subValue, type, arrayType, isSubValueMap, level + 1);
168-
finalValue = this.applyCustomTransformations(finalValue, targetType, transformKey, value, this.transformationType);
214+
let finalValue;
215+
216+
if (this.transformationType === TransformationType.CLASS_TO_PLAIN) {
217+
// Get original value
218+
finalValue = value[transformKey];
219+
// Apply custom transformation
220+
finalValue = this.applyCustomTransformations(finalValue, (targetType as Function), transformKey, value, this.transformationType);
221+
// If nothing change, it means no custom transformation was applied, so use the subValue.
222+
finalValue = (value[transformKey] === finalValue) ? subValue : finalValue;
223+
// Apply the default transformation
224+
finalValue = this.transform(subSource, finalValue, type, arrayType, isSubValueMap, level + 1);
225+
} else {
226+
finalValue = this.transform(subSource, subValue, type, arrayType, isSubValueMap, level + 1);
227+
finalValue = this.applyCustomTransformations(finalValue, (targetType as Function), transformKey, value, this.transformationType);
228+
}
229+
169230
if (newValue instanceof Map) {
170231
newValue.set(newValueKey, finalValue);
171232
} else {
172233
newValue[newValueKey] = finalValue;
173234
}
174235
} else if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
175236
let finalValue = subValue;
176-
finalValue = this.applyCustomTransformations(finalValue, targetType, key, value, this.transformationType);
237+
finalValue = this.applyCustomTransformations(finalValue, (targetType as Function), key, value, this.transformationType);
177238
if (newValue instanceof Map) {
178239
newValue.set(newValueKey, finalValue);
179240
} else {
@@ -182,6 +243,11 @@ export class TransformOperationExecutor {
182243
}
183244

184245
}
246+
247+
if (this.options.enableCircularCheck) {
248+
this.recursionStack.delete(value);
249+
}
250+
185251
return newValue;
186252

187253
} else {
@@ -224,9 +290,8 @@ export class TransformOperationExecutor {
224290
}
225291

226292
// preventing circular references
227-
private isCircular(object: Object, level: number) {
228-
const transformed = this.transformedTypesMap.get(object);
229-
return transformed !== undefined && transformed.level < level;
293+
private isCircular(object: Object) {
294+
return this.recursionStack.has(object);
230295
}
231296

232297
private getReflectedType(target: Function, propertyName: string) {

0 commit comments

Comments
 (0)