@macroforge/mcp-server 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +68 -0
  2. package/dist/index.d.ts +32 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +46 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/tools/docs-loader.d.ts +133 -5
  7. package/dist/tools/docs-loader.d.ts.map +1 -1
  8. package/dist/tools/docs-loader.js +131 -15
  9. package/dist/tools/docs-loader.js.map +1 -1
  10. package/dist/tools/index.d.ts +48 -1
  11. package/dist/tools/index.d.ts.map +1 -1
  12. package/dist/tools/index.js +163 -14
  13. package/dist/tools/index.js.map +1 -1
  14. package/docs/api/api-overview.md +24 -46
  15. package/docs/api/expand-sync.md +24 -51
  16. package/docs/api/native-plugin.md +24 -56
  17. package/docs/api/position-mapper.md +34 -76
  18. package/docs/api/transform-sync.md +27 -59
  19. package/docs/builtin-macros/clone.md +150 -68
  20. package/docs/builtin-macros/debug.md +216 -81
  21. package/docs/builtin-macros/default.md +234 -91
  22. package/docs/builtin-macros/deserialize.md +891 -166
  23. package/docs/builtin-macros/hash.md +238 -82
  24. package/docs/builtin-macros/macros-overview.md +42 -103
  25. package/docs/builtin-macros/ord.md +205 -92
  26. package/docs/builtin-macros/partial-eq.md +178 -97
  27. package/docs/builtin-macros/partial-ord.md +209 -98
  28. package/docs/builtin-macros/serialize.md +326 -137
  29. package/docs/concepts/architecture.md +40 -99
  30. package/docs/concepts/derive-system.md +132 -125
  31. package/docs/concepts/how-macros-work.md +52 -84
  32. package/docs/custom-macros/custom-overview.md +17 -39
  33. package/docs/custom-macros/rust-setup.md +22 -55
  34. package/docs/custom-macros/ts-macro-derive.md +43 -107
  35. package/docs/custom-macros/ts-quote.md +177 -507
  36. package/docs/getting-started/first-macro.md +108 -33
  37. package/docs/getting-started/installation.md +32 -73
  38. package/docs/integration/cli.md +70 -156
  39. package/docs/integration/configuration.md +32 -75
  40. package/docs/integration/integration-overview.md +16 -55
  41. package/docs/integration/mcp-server.md +30 -69
  42. package/docs/integration/svelte-preprocessor.md +60 -83
  43. package/docs/integration/typescript-plugin.md +32 -74
  44. package/docs/integration/vite-plugin.md +30 -79
  45. package/docs/language-servers/ls-overview.md +22 -46
  46. package/docs/language-servers/svelte.md +30 -69
  47. package/docs/language-servers/zed.md +34 -72
  48. package/docs/roadmap/roadmap.md +54 -130
  49. package/docs/sections.json +3 -262
  50. package/package.json +2 -2
@@ -1,12 +1,54 @@
1
1
  # Hash
2
-
3
- *The `Hash` macro generates a `hashCode()` method that computes a numeric hash value based on field values.*
4
-
5
- ## Basic Usage
6
-
7
- <MacroExample before={data.examples.basic.before} after={data.examples.basic.after} />
8
-
9
- ```typescript
2
+ *The `Hash` macro generates a `hashCode()` method for computing numeric hash codes. This is analogous to Rust's `Hash` trait and Java's `hashCode()` method, enabling objects to be used as keys in hash-based collections.*
3
+ ## Basic Usage
4
+ **Before:**
5
+ ```
6
+ /** @derive(Hash) */
7
+ class Point {
8
+ x: number;
9
+ y: number;
10
+
11
+ constructor(x: number, y: number) {
12
+ this.x = x;
13
+ this.y = y;
14
+ }
15
+ }
16
+ ```
17
+ **After:**
18
+ ```
19
+ class Point {
20
+ x: number;
21
+ y: number;
22
+
23
+ constructor(x: number, y: number) {
24
+ this.x = x;
25
+ this.y = y;
26
+ }
27
+
28
+ hashCode(): number {
29
+ let hash = 17;
30
+ hash =
31
+ (hash * 31 +
32
+ (Number.isInteger(this.x)
33
+ ? this.x | 0
34
+ : this.x
35
+ .toString()
36
+ .split('')
37
+ .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
38
+ 0;
39
+ hash =
40
+ (hash * 31 +
41
+ (Number.isInteger(this.y)
42
+ ? this.y | 0
43
+ : this.y
44
+ .toString()
45
+ .split('')
46
+ .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
47
+ 0;
48
+ return hash;
49
+ }
50
+ }
51
+ ``` ```
10
52
  const p1 = new Point(10, 20);
11
53
  const p2 = new Point(10, 20);
12
54
  const p3 = new Point(5, 5);
@@ -14,52 +56,80 @@ const p3 = new Point(5, 5);
14
56
  console.log(p1.hashCode()); // Same hash
15
57
  console.log(p2.hashCode()); // Same hash (equal values = equal hash)
16
58
  console.log(p3.hashCode()); // Different hash
59
+ ``` ## Hash Algorithm
60
+ The generated hash function uses the following algorithm for different types:
61
+ - `number` → Integers use bitwise OR (`| 0`), floats are stringified and hashed
62
+ - `string` → Character-by-character hash: `(h * 31 + charCode) | 0`
63
+ - `boolean` → `1231` for true, `1237` for false (Java convention)
64
+ - `bigint` → Converted to string and hashed character-by-character
65
+ - `Date` → Uses `getTime() | 0` for timestamp hash
66
+ - `Array` → Combines element hashes with `h * 31 + elementHash`
67
+ - `Map/Set` → Combines all entry hashes
68
+ - `Object` → Calls `hashCode()` if available, otherwise JSON stringifies and hashes
69
+ - `null` → Returns 0
70
+ - `undefined` → Returns 1
71
+ ## Field Options
72
+ ### @hash(skip)
73
+ Use `@hash(skip)` to exclude a field from hash computation:
74
+ **Before:**
17
75
  ```
18
-
19
- ## Hash Algorithm
20
-
21
- The generated hash function uses the following algorithm for different types:
22
-
23
- - `number` → Integers use bitwise OR (`| 0`), floats are stringified and hashed
24
-
25
- - `string` → Character-by-character hash: `(h * 31 + charCode) | 0`
26
-
27
- - `boolean` → `1231` for true, `1237` for false (Java convention)
28
-
29
- - `bigint` → Converted to string and hashed character-by-character
30
-
31
- - `Date` → Uses `getTime() | 0` for timestamp hash
32
-
33
- - `Array` → Combines element hashes with `h * 31 + elementHash`
34
-
35
- - `Map/Set` → Combines all entry hashes
36
-
37
- - `Object` → Calls `hashCode()` if available, otherwise JSON stringifies and hashes
38
-
39
- - `null` → Returns 0
40
-
41
- - `undefined` Returns 1
42
-
43
- ## Field Options
44
-
45
- ### @hash(skip)
46
-
47
- Use `@hash(skip)` to exclude a field from hash computation:
48
-
49
- <MacroExample before={data.examples.skip.before} after={data.examples.skip.after} />
50
-
51
- ```typescript
76
+ /** @derive(Hash) */
77
+ class User {
78
+ id: number;
79
+ name: string;
80
+
81
+ /** @hash(skip) */
82
+ lastLogin: Date;
83
+
84
+ constructor(id: number, name: string, lastLogin: Date) {
85
+ this.id = id;
86
+ this.name = name;
87
+ this.lastLogin = lastLogin;
88
+ }
89
+ }
90
+ ```
91
+ **After:**
92
+ ```
93
+ class User {
94
+ id: number;
95
+ name: string;
96
+
97
+ lastLogin: Date;
98
+
99
+ constructor(id: number, name: string, lastLogin: Date) {
100
+ this.id = id;
101
+ this.name = name;
102
+ this.lastLogin = lastLogin;
103
+ }
104
+
105
+ hashCode(): number {
106
+ let hash = 17;
107
+ hash =
108
+ (hash * 31 +
109
+ (Number.isInteger(this.id)
110
+ ? this.id | 0
111
+ : this.id
112
+ .toString()
113
+ .split('')
114
+ .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
115
+ 0;
116
+ hash =
117
+ (hash * 31 +
118
+ (this.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
119
+ 0;
120
+ return hash;
121
+ }
122
+ }
123
+ ``` ```
52
124
  const user1 = new User(1, "Alice", new Date("2024-01-01"));
53
125
  const user2 = new User(1, "Alice", new Date("2024-12-01"));
54
126
 
55
127
  console.log(user1.hashCode() === user2.hashCode()); // true (lastLogin is skipped)
128
+ ``` ## Use with PartialEq
129
+ Hash is often used together with PartialEq. Objects that are equal should have the same hash code:
130
+ **Source:**
56
131
  ```
57
-
58
- ## Use with PartialEq
59
-
60
- Hash is often used together with PartialEq. Objects that are equal should have the same hash code:
61
-
62
- <InteractiveMacro code={`/** @derive(Hash, PartialEq) */
132
+ /** @derive(Hash, PartialEq) */
63
133
  class Product {
64
134
  sku: string;
65
135
  name: string;
@@ -68,56 +138,142 @@ class Product {
68
138
  this.sku = sku;
69
139
  this.name = name;
70
140
  }
71
- }`} />
72
-
73
- ```typescript
141
+ }
142
+ ``` ```
74
143
  const p1 = new Product("ABC123", "Widget");
75
144
  const p2 = new Product("ABC123", "Widget");
76
145
 
77
146
  // Equal objects have equal hash codes
78
147
  console.log(p1.equals(p2)); // true
79
148
  console.log(p1.hashCode() === p2.hashCode()); // true
149
+ ``` ## Interface Support
150
+ Hash also works with interfaces. For interfaces, a namespace is generated with a `hashCode` function:
151
+ **Before:**
80
152
  ```
81
-
82
- ## Interface Support
83
-
84
- Hash also works with interfaces. For interfaces, a namespace is generated with a `hashCode` function:
85
-
86
- <MacroExample before={data.examples.interface.before} after={data.examples.interface.after} />
87
-
88
- ```typescript
153
+ /** @derive(Hash) */
154
+ interface Point {
155
+ x: number;
156
+ y: number;
157
+ }
158
+ ```
159
+ **After:**
160
+ ```
161
+ interface Point {
162
+ x: number;
163
+ y: number;
164
+ }
165
+
166
+ export namespace Point {
167
+ export function hashCode(self: Point): number {
168
+ let hash = 17;
169
+ hash =
170
+ (hash * 31 +
171
+ (Number.isInteger(self.x)
172
+ ? self.x | 0
173
+ : self.x
174
+ .toString()
175
+ .split('')
176
+ .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
177
+ 0;
178
+ hash =
179
+ (hash * 31 +
180
+ (Number.isInteger(self.y)
181
+ ? self.y | 0
182
+ : self.y
183
+ .toString()
184
+ .split('')
185
+ .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
186
+ 0;
187
+ return hash;
188
+ }
189
+ }
190
+ ``` ```
89
191
  const p: Point = { x: 10, y: 20 };
90
192
  console.log(Point.hashCode(p)); // numeric hash value
193
+ ``` ## Enum Support
194
+ Hash works with enums. For string enums, it hashes the string value; for numeric enums, it uses the numeric value directly:
195
+ **Before:**
91
196
  ```
92
-
93
- ## Enum Support
94
-
95
- Hash works with enums. For string enums, it hashes the string value; for numeric enums, it uses the numeric value directly:
96
-
97
- <MacroExample before={data.examples.enum.before} after={data.examples.enum.after} />
98
-
99
- ```typescript
197
+ /** @derive(Hash) */
198
+ enum Status {
199
+ Active = 'active',
200
+ Inactive = 'inactive',
201
+ Pending = 'pending'
202
+ }
203
+ ```
204
+ **After:**
205
+ ```
206
+ enum Status {
207
+ Active = 'active',
208
+ Inactive = 'inactive',
209
+ Pending = 'pending'
210
+ }
211
+
212
+ export namespace Status {
213
+ export function hashCode(value: Status): number {
214
+ if (typeof value === 'string') {
215
+ let hash = 0;
216
+ for (let i = 0; i < value.length; i++) {
217
+ hash = (hash * 31 + value.charCodeAt(i)) | 0;
218
+ }
219
+ return hash;
220
+ }
221
+ return value as number;
222
+ }
223
+ }
224
+ ``` ```
100
225
  console.log(Status.hashCode(Status.Active)); // consistent hash
101
226
  console.log(Status.hashCode(Status.Inactive)); // different hash
227
+ ``` ## Type Alias Support
228
+ Hash works with type aliases. For object types, it hashes each field:
229
+ **Before:**
102
230
  ```
103
-
104
- ## Type Alias Support
105
-
106
- Hash works with type aliases. For object types, it hashes each field:
107
-
108
- <MacroExample before={data.examples.typeAlias.before} after={data.examples.typeAlias.after} />
109
-
110
- ```typescript
231
+ /** @derive(Hash) */
232
+ type Coordinates = {
233
+ lat: number;
234
+ lng: number;
235
+ };
236
+ ```
237
+ **After:**
238
+ ```
239
+ type Coordinates = {
240
+ lat: number;
241
+ lng: number;
242
+ };
243
+
244
+ export namespace Coordinates {
245
+ export function hashCode(value: Coordinates): number {
246
+ let hash = 17;
247
+ hash =
248
+ (hash * 31 +
249
+ (Number.isInteger(value.lat)
250
+ ? value.lat | 0
251
+ : value.lat
252
+ .toString()
253
+ .split('')
254
+ .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
255
+ 0;
256
+ hash =
257
+ (hash * 31 +
258
+ (Number.isInteger(value.lng)
259
+ ? value.lng | 0
260
+ : value.lng
261
+ .toString()
262
+ .split('')
263
+ .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
264
+ 0;
265
+ return hash;
266
+ }
267
+ }
268
+ ``` ```
111
269
  const loc: Coordinates = { lat: 40.7128, lng: -74.0060 };
112
270
  console.log(Coordinates.hashCode(loc));
271
+ ``` For union types, it uses JSON stringification as a fallback:
272
+ **Source:**
113
273
  ```
114
-
115
- For union types, it uses JSON stringification as a fallback:
116
-
117
- <InteractiveMacro code={`/** @derive(Hash) */
118
- type Result = "success" | "error" | "pending";`} />
119
-
120
- ```typescript
274
+ /** @derive(Hash) */
275
+ type Result = "success" | "error" | "pending";
276
+ ``` ```
121
277
  console.log(Result.hashCode("success")); // hash of "success" string
122
278
  console.log(Result.hashCode("error")); // hash of "error" string
123
279
  ```
@@ -1,50 +1,20 @@
1
1
  # Built-in Macros
2
-
3
- *Macroforge comes with built-in derive macros that cover the most common code generation needs. All macros work with classes, interfaces, enums, and type aliases.*
4
-
5
- ## Overview
6
-
7
- | [`Debug`]({base}/docs/builtin-macros/debug)
8
- | `toString(): string`
9
- | Human-readable string representation
10
-
11
- | [`Clone`]({base}/docs/builtin-macros/clone)
12
- | `clone(): T`
13
- | Creates a deep copy of the object
14
-
15
- | [`Default`]({base}/docs/builtin-macros/default)
16
- | `static default(): T`
17
- | Creates an instance with default values
18
-
19
- | [`Hash`]({base}/docs/builtin-macros/hash)
20
- | `hashCode(): number`
21
- | Generates a hash code for the object
22
-
23
- | [`PartialEq`]({base}/docs/builtin-macros/partial-eq)
24
- | `equals(other: T): boolean`
25
- | Value equality comparison
26
-
27
- | [`Ord`]({base}/docs/builtin-macros/ord)
28
- | `compare(other: T): number`
29
- | Total ordering comparison (-1, 0, 1)
30
-
31
- | [`PartialOrd`]({base}/docs/builtin-macros/partial-ord)
32
- | `partialCompare(other: T): number | null`
33
- | Partial ordering comparison
34
-
35
- | [`Serialize`]({base}/docs/builtin-macros/serialize)
36
- | `toJSON(): Record<string, unknown>`
37
- | JSON serialization with type handling
38
-
39
- | [`Deserialize`]({base}/docs/builtin-macros/deserialize)
40
- | `static fromJSON(data: unknown): T`
41
- | JSON deserialization with validation
42
-
43
- ## Using Built-in Macros
44
-
45
- Built-in macros don't require imports. Just use them with `@derive`:
46
-
47
- ```typescript
2
+ *Macroforge comes with built-in derive macros that cover the most common code generation needs. All macros work with classes, interfaces, enums, and type aliases.*
3
+ ## Overview
4
+ | Macro | Generates | Description |
5
+ | --- | --- | --- |
6
+ | [`Debug`](../docs/builtin-macros/debug) | `toString(): string` | Human-readable string representation |
7
+ | [`Clone`](../docs/builtin-macros/clone) | `clone(): T` | Creates a deep copy of the object |
8
+ | [`Default`](../docs/builtin-macros/default) | `static default(): T` | Creates an instance with default values |
9
+ | [`Hash`](../docs/builtin-macros/hash) | `hashCode(): number` | Generates a hash code for the object |
10
+ | [`PartialEq`](../docs/builtin-macros/partial-eq) | `equals(other: T): boolean` | Value equality comparison |
11
+ | [`Ord`](../docs/builtin-macros/ord) | `compare(other: T): number` | Total ordering comparison (-1, 0, 1) |
12
+ | [`PartialOrd`](../docs/builtin-macros/partial-ord) | `partialCompare(other: T): number | null` | Partial ordering comparison |
13
+ | [`Serialize`](../docs/builtin-macros/serialize) | `toJSON(): Record<string, unknown>` | JSON serialization with type handling |
14
+ | [`Deserialize`](../docs/builtin-macros/deserialize) | `static fromJSON(data: unknown): T` | JSON deserialization with validation |
15
+ ## Using Built-in Macros
16
+ Built-in macros don't require imports. Just use them with `@derive`:
17
+ ```
48
18
  /** @derive(Debug, Clone, PartialEq) */
49
19
  class User {
50
20
  name: string;
@@ -55,13 +25,9 @@ class User {
55
25
  this.age = age;
56
26
  }
57
27
  }
58
- ```
59
-
60
- ## Interface Support
61
-
62
- All built-in macros work with interfaces. For interfaces, methods are generated as functions in a namespace with the same name, using `self` as the first parameter:
63
-
64
- ```typescript
28
+ ``` ## Interface Support
29
+ All built-in macros work with interfaces. For interfaces, methods are generated as functions in a namespace with the same name, using `self` as the first parameter:
30
+ ```
65
31
  /** @derive(Debug, Clone, PartialEq) */
66
32
  interface Point {
67
33
  x: number;
@@ -82,13 +48,9 @@ const point: Point = { x: 10, y: 20 };
82
48
  console.log(Point.toString(point)); // "Point { x: 10, y: 20 }"
83
49
  const copy = Point.clone(point); // { x: 10, y: 20 }
84
50
  console.log(Point.equals(point, copy)); // true
85
- ```
86
-
87
- ## Enum Support
88
-
89
- All built-in macros work with enums. For enums, methods are generated as functions in a namespace with the same name:
90
-
91
- ```typescript
51
+ ``` ## Enum Support
52
+ All built-in macros work with enums. For enums, methods are generated as functions in a namespace with the same name:
53
+ ```
92
54
  /** @derive(Debug, Clone, PartialEq, Serialize, Deserialize) */
93
55
  enum Status {
94
56
  Active = "active",
@@ -111,13 +73,9 @@ console.log(Status.toString(Status.Active)); // "Status.Active"
111
73
  console.log(Status.equals(Status.Active, Status.Active)); // true
112
74
  const json = Status.toJSON(Status.Pending); // "pending"
113
75
  const parsed = Status.fromJSON("active"); // Status.Active
114
- ```
115
-
116
- ## Type Alias Support
117
-
118
- All built-in macros work with type aliases. For object type aliases, field-aware methods are generated in a namespace:
119
-
120
- ```typescript
76
+ ``` ## Type Alias Support
77
+ All built-in macros work with type aliases. For object type aliases, field-aware methods are generated in a namespace:
78
+ ```
121
79
  /** @derive(Debug, Clone, PartialEq, Serialize, Deserialize) */
122
80
  type Point = {
123
81
  x: number;
@@ -138,24 +96,17 @@ const point: Point = { x: 10, y: 20 };
138
96
  console.log(Point.toString(point)); // "Point { x: 10, y: 20 }"
139
97
  const copy = Point.clone(point); // { x: 10, y: 20 }
140
98
  console.log(Point.equals(point, copy)); // true
141
- ```
142
-
143
- Union type aliases also work, using JSON-based implementations:
144
-
145
- ```typescript
99
+ ``` Union type aliases also work, using JSON-based implementations:
100
+ ```
146
101
  /** @derive(Debug, PartialEq) */
147
102
  type ApiStatus = "loading" | "success" | "error";
148
103
 
149
104
  const status: ApiStatus = "success";
150
- console.log(ApiStatus.toString(status)); // "ApiStatus(\\"success\\")"
105
+ console.log(ApiStatus.toString(status)); // "ApiStatus(\"success\")"
151
106
  console.log(ApiStatus.equals("success", "success")); // true
152
- ```
153
-
154
- ## Combining Macros
155
-
156
- All macros can be used together. They don't conflict and each generates independent methods:
157
-
158
- ```typescript
107
+ ``` ## Combining Macros
108
+ All macros can be used together. They don't conflict and each generates independent methods:
109
+ ```
159
110
  const user = new User("Alice", 30);
160
111
 
161
112
  // Debug
@@ -168,26 +119,14 @@ console.log(copy.name); // "Alice"
168
119
 
169
120
  // Eq
170
121
  console.log(user.equals(copy)); // true
171
- ```
172
-
173
- ## Detailed Documentation
174
-
175
- Each macro has its own options and behaviors:
176
-
177
- - [**Debug**]({base}/docs/builtin-macros/debug) - Customizable field renaming and skipping
178
-
179
- - [**Clone**]({base}/docs/builtin-macros/clone) - Deep copying for all field types
180
-
181
- - [**Default**]({base}/docs/builtin-macros/default) - Default value generation with field attributes
182
-
183
- - [**Hash**]({base}/docs/builtin-macros/hash) - Hash code generation for use in maps and sets
184
-
185
- - [**PartialEq**]({base}/docs/builtin-macros/partial-eq) - Value-based equality comparison
186
-
187
- - [**Ord**]({base}/docs/builtin-macros/ord) - Total ordering for sorting
188
-
189
- - [**PartialOrd**]({base}/docs/builtin-macros/partial-ord) - Partial ordering comparison
190
-
191
- - [**Serialize**]({base}/docs/builtin-macros/serialize) - JSON serialization with serde-style options
192
-
193
- - [**Deserialize**]({base}/docs/builtin-macros/deserialize) - JSON deserialization with validation
122
+ ``` ## Detailed Documentation
123
+ Each macro has its own options and behaviors:
124
+ - [**Debug**](../docs/builtin-macros/debug) - Customizable field renaming and skipping
125
+ - [**Clone**](../docs/builtin-macros/clone) - Deep copying for all field types
126
+ - [**Default**](../docs/builtin-macros/default) - Default value generation with field attributes
127
+ - [**Hash**](../docs/builtin-macros/hash) - Hash code generation for use in maps and sets
128
+ - [**PartialEq**](../docs/builtin-macros/partial-eq) - Value-based equality comparison
129
+ - [**Ord**](../docs/builtin-macros/ord) - Total ordering for sorting
130
+ - [**PartialOrd**](../docs/builtin-macros/partial-ord) - Partial ordering comparison
131
+ - [**Serialize**](../docs/builtin-macros/serialize) - JSON serialization with serde-style options
132
+ - [**Deserialize**](../docs/builtin-macros/deserialize) - JSON deserialization with validation