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.