Skip to content

Commit d723228

Browse files
committed
feat: enhance fromJSON function to accept class constructors for deserialization
1 parent 6e22b6b commit d723228

File tree

3 files changed

+105
-11
lines changed

3 files changed

+105
-11
lines changed

README.md

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@ The library provides standalone utility functions `fromJSON` and `toJSON` that c
298298

299299
fromJSON Function:
300300

301-
The `fromJSON` function populates an existing object instance with data from JSON, using decorator metadata for type conversion.
301+
The `fromJSON` function deserializes JSON data into a class instance. It can accept either an existing object instance or a class constructor.
302+
303+
**Usage with instance:**
302304

303305
```typescript
304306
import { fromJSON, jsonProperty } from "ts-serializable";
@@ -320,6 +322,7 @@ const json = {
320322
releaseDate: "2024-01-15T10:00:00.000Z"
321323
};
322324

325+
// Pass an existing instance
323326
const product = new Product();
324327
fromJSON(product, json);
325328

@@ -328,13 +331,26 @@ console.log(product.price); // 999.99
328331
console.log(product.releaseDate instanceof Date); // true
329332
```
330333

334+
**Usage with class constructor:**
335+
336+
```typescript
337+
// Pass a class constructor - the function will create an instance automatically
338+
const product = fromJSON(Product, json);
339+
340+
console.log(product instanceof Product); // true
341+
console.log(product.name); // "Laptop"
342+
console.log(product.price); // 999.99
343+
```
344+
331345
Benefits:
332346

333347
- Works with plain classes (no need to extend `Serializable`)
348+
- Accepts both instance and constructor for flexibility
334349
- Respects all decorators (`@jsonProperty`, `@jsonName`, `@jsonIgnore`)
335350
- Supports naming strategies
336351
- Handles nested objects and arrays
337352
- Type-safe deserialization
353+
- Perfect for generic programming patterns
338354

339355
toJSON Function:
340356

@@ -398,13 +414,34 @@ class ApiRequest {
398414
public userTags: string[] = [];
399415
}
400416

401-
// Deserialize from API response
417+
// Deserialize from API response using constructor
402418
const apiData = {
403419
request_id: "REQ-12345",
404420
user_name: "john_doe",
405421
user_tags: ["premium", "verified"]
406422
};
407423

424+
// Using class constructor - creates new instance automatically
425+
const request = fromJSON(ApiRequest, apiData);
426+
427+
console.log(request instanceof ApiRequest); // true
428+
console.log(request.requestId); // "REQ-12345"
429+
console.log(request.userName); // "john_doe"
430+
431+
// Serialize for sending to API
432+
const jsonToSend = toJSON(request);
433+
console.log(jsonToSend);
434+
// Output: {
435+
// request_id: "REQ-12345",
436+
// user_name: "john_doe",
437+
// user_tags: ["premium", "verified"]
438+
// }
439+
```
440+
441+
**Alternative approach using instance:**
442+
443+
```typescript
444+
// Using instance
408445
const request = new ApiRequest();
409446
fromJSON(request, apiData);
410447

@@ -822,9 +859,22 @@ console.log(team.members[0] instanceof User); // true
822859

823860
### Standalone Functions
824861

825-
- **`fromJSON<T>(obj: T, json: object, settings?: Partial<SerializationSettings>): T`**
862+
- **`fromJSON<T>(obj: T | (new () => T), json: object, settings?: Partial<SerializationSettings>): T`**
863+
864+
Deserializes JSON into an object instance. Accepts either:
865+
- An existing object instance to populate
866+
- A class constructor to create a new instance
867+
868+
**Examples:**
869+
870+
```typescript
871+
// With instance
872+
const product = new Product();
873+
fromJSON(product, jsonData);
826874

827-
Deserializes JSON into an existing object instance.
875+
// With constructor
876+
const product = fromJSON(Product, jsonData);
877+
```
828878

829879
- **`toJSON(obj: Serializable | object): Record<string, unknown>`**
830880

src/functions/FromJSON.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,18 @@ import {onWrongType} from "./OnWrongType.js";
6363
* fromJSON(product, { title: "Laptop" });
6464
* ```
6565
*/
66-
// eslint-disable-next-line max-statements
67-
export const fromJSON = <T extends (Serializable | object)>(obj: T, json: object, settings?: Partial<SerializationSettings>): T => {
66+
// eslint-disable-next-line max-statements, max-lines-per-function
67+
export const fromJSON = <T extends (Serializable | object)>(
68+
obj: T | (new () => T),
69+
json: object,
70+
settings?: Partial<SerializationSettings>
71+
): T => {
6872
const unknownJson: unknown = json;
73+
if (typeof obj === "function") {
74+
const ClassConstructor = obj as new () => T;
75+
// eslint-disable-next-line no-param-reassign
76+
obj = new ClassConstructor();
77+
}
6978

7079
if (
7180
unknownJson === null ||

tests/base-functions.spec.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {UserSimple as IUserSimple, FriendSimple as IFriendSimple} from "./m
99
import {fromJSON} from "../src/functions/FromJSON";
1010

1111
describe("Base functions", () => {
12-
it("user from method fromJSON must be instance of User", async () => {
12+
it("should deserialize JSON to User instance using instance method fromJSON()", async () => {
1313
const {User} = await import("./models/User");
1414
const json = await import("./jsons/json-generator.json", {with: {type: "json"}});
1515

@@ -44,7 +44,7 @@ describe("Base functions", () => {
4444
});
4545
});
4646

47-
it("user from static method fromJSON must be instance of User", async () => {
47+
it("should deserialize JSON to User instance using static method fromJSON()", async () => {
4848
const {User} = await import("./models/User");
4949
const json = await import("./jsons/json-generator.json", {with: {type: "json"}});
5050
const [object] = Reflect.get(json, "default") as typeof json;
@@ -78,7 +78,7 @@ describe("Base functions", () => {
7878
});
7979
});
8080

81-
it("user from method fromString must be instance of User", async () => {
81+
it("should deserialize JSON string to User instance using instance method fromString()", async () => {
8282
const {User} = await import("./models/User");
8383
const json = await import("./jsons/json-generator.json", {with: {type: "json"}});
8484
const [object] = Reflect.get(json, "default") as typeof json;
@@ -112,7 +112,7 @@ describe("Base functions", () => {
112112
});
113113
});
114114

115-
it("user from static method fromString must be instance of User", async () => {
115+
it("should deserialize JSON string to User instance using static method fromString()", async () => {
116116
const {User} = await import("./models/User");
117117
const json = await import("./jsons/json-generator.json", {with: {type: "json"}});
118118
const [object] = Reflect.get(json, "default") as typeof json;
@@ -146,7 +146,7 @@ describe("Base functions", () => {
146146
});
147147
});
148148

149-
it("user from function fromJSON must be instance of User", async () => {
149+
it("should deserialize JSON to plain class instance using standalone fromJSON() function", async () => {
150150
const {UserSimple} = await import("./models/UserSimple");
151151
const json = await import("./jsons/json-generator.json", {with: {type: "json"}});
152152

@@ -180,4 +180,39 @@ describe("Base functions", () => {
180180
assert.strictEqual(friend.name, object.friends[index].name, `friend ${String(index)} name is not equal`);
181181
});
182182
});
183+
184+
it("should deserialize JSON by passing class constructor to standalone fromJSON() function", async () => {
185+
const {UserSimple} = await import("./models/UserSimple");
186+
const json = await import("./jsons/json-generator.json", {with: {type: "json"}});
187+
188+
const [object] = Reflect.get(json, "default") as typeof json;
189+
190+
const user: IUserSimple = fromJSON(UserSimple, object);
191+
192+
assert.isTrue(user instanceof UserSimple);
193+
assert.strictEqual(user.id, object.id, "id is not equal");
194+
assert.strictEqual(user.index, object.index, "index is not equal");
195+
assert.strictEqual(user.guid, object.guid, "guid is not equal");
196+
assert.strictEqual(user.isActive, object.isActive, "isActive is not equal");
197+
assert.strictEqual(user.balance, object.balance, "balance is not equal");
198+
assert.strictEqual(user.picture, object.picture, "picture is not equal");
199+
assert.strictEqual(user.age, object.age, "age is not equal");
200+
assert.strictEqual(user.eyeColor, object.eyeColor, "eyeColor is not equal");
201+
assert.strictEqual(user.name, object.name, "name is not equal");
202+
assert.strictEqual(user.company, object.company, "company is not equal");
203+
assert.strictEqual(user.email, object.email, "email is not equal");
204+
assert.strictEqual(user.phone, object.phone, "phone is not equal");
205+
assert.strictEqual(user.address, object.address, "address is not equal");
206+
assert.strictEqual(user.about, object.about, "about is not equal");
207+
assert.strictEqual(user.latitude, object.latitude, "latitude is not equal");
208+
assert.strictEqual(user.longitude, object.longitude, "longitude is not equal");
209+
assert.deepEqual(user.tags, object.tags, "tags is not equal");
210+
assert.strictEqual(user.greeting, object.greeting, "greeting is not equal");
211+
assert.strictEqual(user.favoriteFruit, object.favoriteFruit, "favoriteFruit is not equal");
212+
213+
user.friends.forEach((friend: IFriendSimple, index: number) => {
214+
assert.strictEqual(friend.id, object.friends[index].id, `friend ${String(index)} id is not equal`);
215+
assert.strictEqual(friend.name, object.friends[index].name, `friend ${String(index)} name is not equal`);
216+
});
217+
});
183218
});

0 commit comments

Comments
 (0)