github.com/kaptinlin/jsonschema@v0.4.6/docs/unmarshal.md (about) 1 # Unmarshal Guide 2 3 The JSON Schema library provides powerful unmarshaling capabilities that apply schema defaults while converting data to Go types. **Validation and unmarshaling are separate operations** for maximum flexibility. 4 5 ## Quick Start 6 7 ```go 8 import "github.com/kaptinlin/jsonschema" 9 10 // Compile schema 11 compiler := jsonschema.NewCompiler() 12 schema, err := compiler.Compile([]byte(`{ 13 "type": "object", 14 "properties": { 15 "name": {"type": "string"}, 16 "country": {"type": "string", "default": "US"}, 17 "active": {"type": "boolean", "default": true} 18 }, 19 "required": ["name"] 20 }`)) 21 22 // Recommended workflow: validate first, then unmarshal 23 data := []byte(`{"name": "John"}`) 24 25 // Step 1: Validate 26 result := schema.Validate(data) 27 if result.IsValid() { 28 // Step 2: Unmarshal with defaults 29 var user User 30 err := schema.Unmarshal(&user, data) 31 if err != nil { 32 log.Fatal(err) 33 } 34 // user.Country = "US", user.Active = true (defaults applied) 35 } else { 36 // Handle validation errors 37 for field, err := range result.Errors { 38 log.Printf("%s: %s", field, err.Message) 39 } 40 } 41 ``` 42 43 ## Key Behavior 44 45 ### ✅ What Unmarshal Does 46 - **Applies default values** from schema 47 - **Converts data types** to match Go struct fields 48 - **Handles multiple input types** (JSON bytes, maps, structs) 49 - **Unmarshals to destination** (structs, maps, slices) 50 51 ### ❌ What Unmarshal Does NOT Do 52 - **Does NOT validate data** against schema constraints 53 - **Does NOT check required fields** 54 - **Does NOT enforce type constraints** 55 56 > **Important**: Always validate data separately before unmarshaling for production use. 57 58 ## Input Types 59 60 ### JSON Bytes 61 ```go 62 data := []byte(`{"name": "John", "age": 25}`) 63 var user User 64 err := schema.Unmarshal(&user, data) 65 ``` 66 67 ### Maps 68 ```go 69 data := map[string]interface{}{ 70 "name": "John", 71 "age": 25, 72 } 73 var user User 74 err := schema.Unmarshal(&user, data) 75 ``` 76 77 ### Structs 78 ```go 79 source := SourceUser{Name: "John", Age: 25} 80 var user User 81 err := schema.Unmarshal(&user, source) 82 ``` 83 84 ## Output Types 85 86 ### Structs 87 ```go 88 type User struct { 89 Name string `json:"name"` 90 Country string `json:"country"` 91 Active bool `json:"active"` 92 } 93 94 var user User 95 err := schema.Unmarshal(&user, data) 96 ``` 97 98 ### Maps 99 ```go 100 var result map[string]interface{} 101 err := schema.Unmarshal(&result, data) 102 ``` 103 104 ### Slices 105 ```go 106 var numbers []int 107 err := schema.Unmarshal(&numbers, []byte(`[1, 2, 3]`)) 108 ``` 109 110 ## Default Values 111 112 The unmarshal process automatically applies default values defined in the schema: 113 114 ```go 115 schema := `{ 116 "type": "object", 117 "properties": { 118 "name": {"type": "string"}, 119 "role": {"type": "string", "default": "user"}, 120 "permissions": { 121 "type": "array", 122 "default": ["read"] 123 }, 124 "settings": { 125 "type": "object", 126 "default": {"theme": "light"}, 127 "properties": { 128 "theme": {"type": "string"}, 129 "notifications": {"type": "boolean", "default": true} 130 } 131 } 132 } 133 }` 134 135 // Input: {"name": "John"} 136 // Result after unmarshal: 137 // { 138 // "name": "John", 139 // "role": "user", 140 // "permissions": ["read"], 141 // "settings": {"theme": "light", "notifications": true} 142 // } 143 ``` 144 145 ## Dynamic Default Values 146 147 The library supports dynamic functions for generating default values at runtime: 148 149 ### Function Registration 150 151 ```go 152 // Register built-in and custom functions 153 compiler := jsonschema.NewCompiler() 154 compiler.RegisterDefaultFunc("now", jsonschema.DefaultNowFunc) 155 compiler.RegisterDefaultFunc("uuid", func(args ...any) (any, error) { 156 return uuid.New().String(), nil 157 }) 158 ``` 159 160 ### Schema with Dynamic Defaults 161 162 ```go 163 schemaJSON := `{ 164 "type": "object", 165 "properties": { 166 "id": {"default": "uuid()"}, 167 "createdAt": {"default": "now()"}, 168 "updatedAt": {"default": "now(2006-01-02 15:04:05)"}, 169 "status": {"default": "active"}, 170 "unregistered": {"default": "unknown_func()"} 171 } 172 }` 173 174 schema, _ := compiler.Compile([]byte(schemaJSON)) 175 176 // Unmarshal with empty input 177 var result map[string]interface{} 178 schema.Unmarshal(&result, map[string]interface{}{}) 179 180 // Output: 181 // { 182 // "id": "3ace637a-515a-4328-a614-b3deb58d410d", 183 // "createdAt": "2025-06-05T01:05:22+08:00", 184 // "updatedAt": "2025-06-05 01:05:22", 185 // "status": "active", 186 // "unregistered": "unknown_func()" // Falls back to literal 187 // } 188 ``` 189 190 ### Built-in Functions 191 192 #### `DefaultNowFunc` 193 Generates timestamps with optional custom formatting: 194 195 ```go 196 // Register the function 197 compiler.RegisterDefaultFunc("now", jsonschema.DefaultNowFunc) 198 199 // Usage in schema 200 "createdAt": {"default": "now()"} // RFC3339 format 201 "date": {"default": "now(2006-01-02)"} // Date only 202 "time": {"default": "now(15:04:05)"} // Time only 203 "custom": {"default": "now(Jan 2, 2006 3:04 PM)"} // Custom format 204 ``` 205 206 ### Per-Schema Compilers 207 208 Use `SetCompiler()` to isolate function registries per schema: 209 210 ```go 211 // Create custom compiler for specific use case 212 apiCompiler := jsonschema.NewCompiler() 213 apiCompiler.RegisterDefaultFunc("apiKey", generateAPIKey) 214 apiCompiler.RegisterDefaultFunc("now", jsonschema.DefaultNowFunc) 215 216 // Apply to programmatically built schema 217 schema := jsonschema.Object( 218 jsonschema.Prop("apiKey", jsonschema.String(jsonschema.Default("apiKey()"))), 219 jsonschema.Prop("timestamp", jsonschema.String(jsonschema.Default("now()"))), 220 ).SetCompiler(apiCompiler) 221 222 // Child schemas inherit parent's compiler automatically 223 ``` 224 225 ### Error Handling 226 227 Dynamic functions are safe-by-design: 228 - **Unregistered functions**: Fall back to literal string values 229 - **Function errors**: Fall back to literal string values 230 - **No panics**: Library never crashes on function failures 231 232 ```go 233 // This won't break unmarshaling 234 schemaJSON := `{ 235 "properties": { 236 "value": {"default": "nonexistent_function()"} 237 } 238 }` 239 240 // result["value"] will be "nonexistent_function()" (literal) 241 ``` 242 243 ## Error Handling 244 245 ```go 246 import "errors" 247 248 var user User 249 err := schema.Unmarshal(&user, data) 250 if err != nil { 251 var unmarshalErr *jsonschema.UnmarshalError 252 if errors.As(err, &unmarshalErr) { 253 switch unmarshalErr.Type { 254 case "destination": 255 log.Printf("Destination error: %s", unmarshalErr.Reason) 256 case "source": 257 log.Printf("Source error: %s", unmarshalErr.Reason) 258 case "defaults": 259 log.Printf("Default application error: %s", unmarshalErr.Reason) 260 case "unmarshal": 261 log.Printf("Unmarshal error: %s", unmarshalErr.Reason) 262 } 263 } 264 } 265 ``` 266 267 ## Validation + Unmarshal Patterns 268 269 ### Pattern 1: Strict Validation 270 ```go 271 result := schema.Validate(data) 272 if !result.IsValid() { 273 return fmt.Errorf("validation failed: %v", result.Errors) 274 } 275 276 var user User 277 return schema.Unmarshal(&user, data) 278 ``` 279 280 ### Pattern 2: Conditional Processing 281 ```go 282 result := schema.Validate(data) 283 var user User 284 err := schema.Unmarshal(&user, data) // Always unmarshal 285 286 if result.IsValid() { 287 // Process valid data 288 return processUser(user) 289 } else { 290 // Log errors but still process with defaults 291 log.Printf("Validation warnings: %v", result.Errors) 292 return processUserWithWarnings(user) 293 } 294 ``` 295 296 ### Pattern 3: Field-Level Error Handling 297 ```go 298 result := schema.Validate(data) 299 var user User 300 schema.Unmarshal(&user, data) 301 302 for field, err := range result.Errors { 303 switch field { 304 case "email": 305 user.Email = "invalid@example.com" // Fallback 306 case "age": 307 user.Age = 18 // Default minimum 308 } 309 } 310 ``` 311 312 ## Performance Tips 313 314 ### Pre-compiled Schemas 315 ```go 316 var userSchema *jsonschema.Schema 317 318 func init() { 319 compiler := jsonschema.NewCompiler() 320 userSchema, _ = compiler.Compile(schemaJSON) 321 } 322 323 func ProcessUser(data []byte) error { 324 result := userSchema.Validate(data) 325 if !result.IsValid() { 326 return fmt.Errorf("invalid data") 327 } 328 329 var user User 330 return userSchema.Unmarshal(&user, data) 331 } 332 ``` 333 334 ### Batch Processing 335 ```go 336 func ProcessUsers(dataList [][]byte) ([]User, error) { 337 users := make([]User, 0, len(dataList)) 338 339 for _, data := range dataList { 340 result := schema.Validate(data) 341 if result.IsValid() { 342 var user User 343 if err := schema.Unmarshal(&user, data); err != nil { 344 return nil, err 345 } 346 users = append(users, user) 347 } 348 } 349 350 return users, nil 351 } 352 ``` 353 354 ## Advanced Use Cases 355 356 ### Custom Time Formats 357 ```go 358 type Event struct { 359 Name string `json:"name"` 360 Timestamp time.Time `json:"timestamp"` 361 } 362 363 schema := `{ 364 "type": "object", 365 "properties": { 366 "name": {"type": "string"}, 367 "timestamp": {"type": "string", "default": "2025-01-01T00:00:00Z"} 368 } 369 }` 370 371 // Automatically parses time strings to time.Time 372 ``` 373 374 ### Nested Structures 375 ```go 376 type User struct { 377 Name string `json:"name"` 378 Profile Profile `json:"profile"` 379 } 380 381 type Profile struct { 382 Age int `json:"age"` 383 Country string `json:"country"` 384 } 385 386 schema := `{ 387 "type": "object", 388 "properties": { 389 "name": {"type": "string"}, 390 "profile": { 391 "type": "object", 392 "properties": { 393 "age": {"type": "integer", "default": 18}, 394 "country": {"type": "string", "default": "US"} 395 } 396 } 397 } 398 }` 399 400 // Applies defaults recursively to nested objects 401 ``` 402 403 ## Migration from Previous Versions 404 405 If you were using the old behavior where `Unmarshal` included validation: 406 407 ### Before (validation + unmarshal combined) 408 ```go 409 var user User 410 err := schema.Unmarshal(&user, data) 411 if err != nil { 412 // Handle both validation and unmarshal errors 413 log.Fatal(err) 414 } 415 ``` 416 417 ### After (validation + unmarshal separate) 418 ```go 419 // Step 1: Validate 420 result := schema.Validate(data) 421 if !result.IsValid() { 422 // Handle validation errors 423 for field, err := range result.Errors { 424 log.Printf("%s: %s", field, err.Message) 425 } 426 return 427 } 428 429 // Step 2: Unmarshal 430 var user User 431 err := schema.Unmarshal(&user, data) 432 if err != nil { 433 // Handle unmarshal errors 434 log.Fatal(err) 435 } 436 ``` 437 438 This separation provides much greater flexibility for error handling and processing workflows.