github.com/kaptinlin/jsonschema@v0.4.6/unmarshal_test.go (about)

     1  package jsonschema
     2  
     3  import (
     4  	"testing"
     5  	"time"
     6  
     7  	"github.com/stretchr/testify/assert"
     8  	"github.com/stretchr/testify/require"
     9  )
    10  
    11  // Test structures
    12  type User struct {
    13  	ID        int       `json:"id"`
    14  	Name      string    `json:"name"`
    15  	Email     *string   `json:"email,omitempty"`
    16  	CreatedAt time.Time `json:"created_at"`
    17  	Active    bool      `json:"active"`
    18  	Score     *float64  `json:"score,omitempty"`
    19  }
    20  
    21  type NestedUser struct {
    22  	ID      int     `json:"id"`
    23  	Name    string  `json:"name"`
    24  	Profile Profile `json:"profile"`
    25  }
    26  
    27  type Profile struct {
    28  	Age     int    `json:"age"`
    29  	Country string `json:"country"`
    30  }
    31  
    32  // TestUnmarshalBasicTypes tests basic unmarshaling with defaults
    33  func TestUnmarshalBasicTypes(t *testing.T) {
    34  	schemaJSON := `{
    35  		"type": "object",
    36  		"properties": {
    37  			"id": {"type": "integer"},
    38  			"name": {"type": "string", "default": "Anonymous"},
    39  			"email": {"type": "string"},
    40  			"created_at": {"type": "string", "format": "date-time", "default": "2025-01-01T00:00:00Z"},
    41  			"active": {"type": "boolean", "default": true},
    42  			"score": {"type": "number"}
    43  		},
    44  		"required": ["id"]
    45  	}`
    46  
    47  	compiler := NewCompiler()
    48  	schema, err := compiler.Compile([]byte(schemaJSON))
    49  	require.NoError(t, err)
    50  
    51  	tests := []struct {
    52  		name     string
    53  		input    interface{}
    54  		expected User
    55  	}{
    56  		{
    57  			name:  "JSON bytes with defaults",
    58  			input: []byte(`{"id": 1}`),
    59  			expected: User{
    60  				ID:        1,
    61  				Name:      "Anonymous",
    62  				CreatedAt: parseTime("2025-01-01T00:00:00Z"),
    63  				Active:    true,
    64  			},
    65  		},
    66  		{
    67  			name: "Map with partial data",
    68  			input: map[string]interface{}{
    69  				"id":   2,
    70  				"name": "John",
    71  			},
    72  			expected: User{
    73  				ID:        2,
    74  				Name:      "John",
    75  				CreatedAt: parseTime("2025-01-01T00:00:00Z"),
    76  				Active:    true,
    77  			},
    78  		},
    79  		{
    80  			name: "Struct input",
    81  			input: struct {
    82  				ID   int    `json:"id"`
    83  				Name string `json:"name"`
    84  			}{ID: 3, Name: "Jane"},
    85  			expected: User{
    86  				ID:        3,
    87  				Name:      "Jane",
    88  				CreatedAt: parseTime("2025-01-01T00:00:00Z"),
    89  				Active:    true,
    90  			},
    91  		},
    92  	}
    93  
    94  	for _, tt := range tests {
    95  		t.Run(tt.name, func(t *testing.T) {
    96  			var result User
    97  			err := schema.Unmarshal(&result, tt.input)
    98  			require.NoError(t, err)
    99  			assert.Equal(t, tt.expected.ID, result.ID)
   100  			assert.Equal(t, tt.expected.Name, result.Name)
   101  			assert.Equal(t, tt.expected.Active, result.Active)
   102  			assert.True(t, tt.expected.CreatedAt.Equal(result.CreatedAt))
   103  		})
   104  	}
   105  }
   106  
   107  // TestUnmarshalPointerFields tests pointer field handling
   108  func TestUnmarshalPointerFields(t *testing.T) {
   109  	schemaJSON := `{
   110  		"type": "object",
   111  		"properties": {
   112  			"id": {"type": "integer"},
   113  			"name": {"type": "string"},
   114  			"email": {"type": "string", "default": "user@example.com"},
   115  			"score": {"type": "number", "default": 100.0}
   116  		},
   117  		"required": ["id", "name"]
   118  	}`
   119  
   120  	compiler := NewCompiler()
   121  	schema, err := compiler.Compile([]byte(schemaJSON))
   122  	require.NoError(t, err)
   123  
   124  	input := `{"id": 1, "name": "John"}`
   125  	var result User
   126  	err = schema.Unmarshal(&result, []byte(input))
   127  	require.NoError(t, err)
   128  
   129  	assert.Equal(t, 1, result.ID)
   130  	assert.Equal(t, "John", result.Name)
   131  	assert.NotNil(t, result.Email)
   132  	assert.Equal(t, "user@example.com", *result.Email)
   133  	assert.NotNil(t, result.Score)
   134  	assert.Equal(t, 100.0, *result.Score)
   135  }
   136  
   137  // TestUnmarshalNestedStructs tests nested struct unmarshaling
   138  func TestUnmarshalNestedStructs(t *testing.T) {
   139  	schemaJSON := `{
   140  		"type": "object",
   141  		"properties": {
   142  			"id": {"type": "integer"},
   143  			"name": {"type": "string"},
   144  			"profile": {
   145  				"type": "object",
   146  				"properties": {
   147  					"age": {"type": "integer", "default": 18},
   148  					"country": {"type": "string", "default": "US"}
   149  				}
   150  			}
   151  		},
   152  		"required": ["id", "name"]
   153  	}`
   154  
   155  	compiler := NewCompiler()
   156  	schema, err := compiler.Compile([]byte(schemaJSON))
   157  	require.NoError(t, err)
   158  
   159  	input := `{"id": 1, "name": "John", "profile": {"age": 25}}`
   160  	var result NestedUser
   161  	err = schema.Unmarshal(&result, []byte(input))
   162  	require.NoError(t, err)
   163  
   164  	assert.Equal(t, 1, result.ID)
   165  	assert.Equal(t, "John", result.Name)
   166  	assert.Equal(t, 25, result.Profile.Age)
   167  	assert.Equal(t, "US", result.Profile.Country) // Default applied
   168  }
   169  
   170  // TestUnmarshalToMap tests unmarshaling to map
   171  func TestUnmarshalToMap(t *testing.T) {
   172  	schemaJSON := `{
   173  		"type": "object",
   174  		"properties": {
   175  			"id": {"type": "integer"},
   176  			"name": {"type": "string", "default": "Anonymous"},
   177  			"active": {"type": "boolean", "default": true}
   178  		},
   179  		"required": ["id"]
   180  	}`
   181  
   182  	compiler := NewCompiler()
   183  	schema, err := compiler.Compile([]byte(schemaJSON))
   184  	require.NoError(t, err)
   185  
   186  	input := `{"id": 1}`
   187  	var result map[string]interface{}
   188  	err = schema.Unmarshal(&result, []byte(input))
   189  	require.NoError(t, err)
   190  
   191  	assert.Equal(t, float64(1), result["id"]) // JSON numbers are float64
   192  	assert.Equal(t, "Anonymous", result["name"])
   193  	assert.Equal(t, true, result["active"])
   194  }
   195  
   196  // TestUnmarshalWithoutValidation tests that unmarshal works without validation
   197  func TestUnmarshalWithoutValidation(t *testing.T) {
   198  	schemaJSON := `{
   199  		"type": "object",
   200  		"properties": {
   201  			"id": {"type": "integer", "minimum": 1}
   202  		},
   203  		"required": ["id"]
   204  	}`
   205  
   206  	compiler := NewCompiler()
   207  	schema, err := compiler.Compile([]byte(schemaJSON))
   208  	require.NoError(t, err)
   209  
   210  	// This violates minimum constraint but unmarshal should still work
   211  	input := `{"id": 0}`
   212  	var result User
   213  	err = schema.Unmarshal(&result, []byte(input))
   214  	require.NoError(t, err) // No error because validation is not performed
   215  	assert.Equal(t, 0, result.ID)
   216  }
   217  
   218  // TestSeparateValidationAndUnmarshal tests the intended workflow: validate first, then unmarshal
   219  func TestSeparateValidationAndUnmarshal(t *testing.T) {
   220  	schemaJSON := `{
   221  		"type": "object",
   222  		"properties": {
   223  			"id": {"type": "integer", "minimum": 1},
   224  			"name": {"type": "string", "default": "Anonymous"}
   225  		},
   226  		"required": ["id"]
   227  	}`
   228  
   229  	compiler := NewCompiler()
   230  	schema, err := compiler.Compile([]byte(schemaJSON))
   231  	require.NoError(t, err)
   232  
   233  	tests := []struct {
   234  		name           string
   235  		input          string
   236  		shouldValidate bool
   237  		expectedID     int
   238  		expectedName   string
   239  	}{
   240  		{
   241  			name:           "valid data",
   242  			input:          `{"id": 5}`,
   243  			shouldValidate: true,
   244  			expectedID:     5,
   245  			expectedName:   "Anonymous",
   246  		},
   247  		{
   248  			name:           "invalid data (but unmarshal works)",
   249  			input:          `{"id": 0}`,
   250  			shouldValidate: false,
   251  			expectedID:     0,
   252  			expectedName:   "Anonymous",
   253  		},
   254  	}
   255  
   256  	for _, tt := range tests {
   257  		t.Run(tt.name, func(t *testing.T) {
   258  			// Step 1: Validate
   259  			result := schema.Validate([]byte(tt.input))
   260  			assert.Equal(t, tt.shouldValidate, result.IsValid())
   261  
   262  			// Step 2: Unmarshal (works regardless of validation result)
   263  			var user User
   264  			err := schema.Unmarshal(&user, []byte(tt.input))
   265  			require.NoError(t, err)
   266  			assert.Equal(t, tt.expectedID, user.ID)
   267  			assert.Equal(t, tt.expectedName, user.Name)
   268  
   269  			// Step 3: Handle based on validation result
   270  			if result.IsValid() {
   271  				// Proceed with valid data
   272  				assert.Equal(t, tt.expectedID, user.ID)
   273  			} else {
   274  				// Handle validation errors
   275  				assert.Contains(t, result.Errors, "properties")
   276  			}
   277  		})
   278  	}
   279  }
   280  
   281  // TestWorkflowExample demonstrates the recommended usage pattern
   282  func TestWorkflowExample(t *testing.T) {
   283  	schemaJSON := `{
   284  		"type": "object",
   285  		"properties": {
   286  			"user_id": {"type": "integer", "minimum": 1},
   287  			"email": {"type": "string", "format": "email"},
   288  			"country": {"type": "string", "default": "US"},
   289  			"active": {"type": "boolean", "default": true}
   290  		},
   291  		"required": ["user_id", "email"]
   292  	}`
   293  
   294  	compiler := NewCompiler()
   295  	schema, err := compiler.Compile([]byte(schemaJSON))
   296  	require.NoError(t, err)
   297  
   298  	type UserProfile struct {
   299  		UserID  int    `json:"user_id"`
   300  		Email   string `json:"email"`
   301  		Country string `json:"country"`
   302  		Active  bool   `json:"active"`
   303  	}
   304  
   305  	input := []byte(`{"user_id": 123, "email": "user@example.com"}`)
   306  
   307  	// Recommended workflow
   308  	result := schema.Validate(input)
   309  	if result.IsValid() {
   310  		var profile UserProfile
   311  		err := schema.Unmarshal(&profile, input)
   312  		require.NoError(t, err)
   313  
   314  		assert.Equal(t, 123, profile.UserID)
   315  		assert.Equal(t, "user@example.com", profile.Email)
   316  		assert.Equal(t, "US", profile.Country) // Default applied
   317  		assert.Equal(t, true, profile.Active)  // Default applied
   318  	} else {
   319  		t.Fatalf("Validation failed: %v", result.Errors)
   320  	}
   321  }
   322  
   323  // TestUnmarshalErrorCases tests various error conditions
   324  func TestUnmarshalErrorCases(t *testing.T) {
   325  	schemaJSON := `{
   326  		"type": "object",
   327  		"properties": {
   328  			"id": {"type": "integer"}
   329  		}
   330  	}`
   331  
   332  	compiler := NewCompiler()
   333  	schema, err := compiler.Compile([]byte(schemaJSON))
   334  	require.NoError(t, err)
   335  
   336  	tests := []struct {
   337  		name    string
   338  		dst     interface{}
   339  		src     interface{}
   340  		errType string
   341  	}{
   342  		{
   343  			name:    "nil destination",
   344  			dst:     nil,
   345  			src:     `{"id": 1}`,
   346  			errType: "destination",
   347  		},
   348  		{
   349  			name:    "non-pointer destination",
   350  			dst:     User{},
   351  			src:     `{"id": 1}`,
   352  			errType: "destination",
   353  		},
   354  		{
   355  			name:    "nil pointer destination",
   356  			dst:     (*User)(nil),
   357  			src:     `{"id": 1}`,
   358  			errType: "destination",
   359  		},
   360  		{
   361  			name:    "invalid JSON source",
   362  			dst:     &User{},
   363  			src:     []byte(`{invalid json}`),
   364  			errType: "source",
   365  		},
   366  	}
   367  
   368  	for _, tt := range tests {
   369  		t.Run(tt.name, func(t *testing.T) {
   370  			err := schema.Unmarshal(tt.dst, tt.src)
   371  			require.Error(t, err)
   372  
   373  			var unmarshalErr *UnmarshalError
   374  			require.ErrorAs(t, err, &unmarshalErr)
   375  			assert.Equal(t, tt.errType, unmarshalErr.Type)
   376  		})
   377  	}
   378  }
   379  
   380  // TestUnmarshalTimeHandling tests time parsing
   381  func TestUnmarshalTimeHandling(t *testing.T) {
   382  	schemaJSON := `{
   383  		"type": "object",
   384  		"properties": {
   385  			"id": {"type": "integer"},
   386  			"created_at": {"type": "string", "format": "date-time"}
   387  		},
   388  		"required": ["id"]
   389  	}`
   390  
   391  	compiler := NewCompiler()
   392  	schema, err := compiler.Compile([]byte(schemaJSON))
   393  	require.NoError(t, err)
   394  
   395  	tests := []struct {
   396  		name        string
   397  		timeString  string
   398  		expectError bool
   399  	}{
   400  		{"RFC3339", "2025-01-01T12:00:00Z", false},
   401  		{"RFC3339Nano", "2025-01-01T12:00:00.123456789Z", false},
   402  		{"Date only", "2025-01-01", false},
   403  		{"Invalid format", "not-a-date", true},
   404  	}
   405  
   406  	for _, tt := range tests {
   407  		t.Run(tt.name, func(t *testing.T) {
   408  			input := map[string]interface{}{
   409  				"id":         1,
   410  				"created_at": tt.timeString,
   411  			}
   412  
   413  			var result User
   414  			err := schema.Unmarshal(&result, input)
   415  
   416  			if tt.expectError {
   417  				require.Error(t, err)
   418  			} else {
   419  				require.NoError(t, err)
   420  				assert.False(t, result.CreatedAt.IsZero())
   421  			}
   422  		})
   423  	}
   424  }
   425  
   426  // TestUnmarshalInputTypes tests various input types
   427  func TestUnmarshalInputTypes(t *testing.T) {
   428  	schemaJSON := `{
   429  		"type": "object",
   430  		"properties": {
   431  			"id": {"type": "integer"},
   432  			"name": {"type": "string", "default": "Unknown"}
   433  		},
   434  		"required": ["id"]
   435  	}`
   436  
   437  	compiler := NewCompiler()
   438  	schema, err := compiler.Compile([]byte(schemaJSON))
   439  	require.NoError(t, err)
   440  
   441  	tests := []struct {
   442  		name     string
   443  		input    interface{}
   444  		expected User
   445  	}{
   446  		{
   447  			name:  "JSON bytes",
   448  			input: []byte(`{"id": 1}`),
   449  			expected: User{
   450  				ID:   1,
   451  				Name: "Unknown",
   452  			},
   453  		},
   454  		{
   455  			name: "Map input",
   456  			input: map[string]interface{}{
   457  				"id": 3,
   458  			},
   459  			expected: User{
   460  				ID:   3,
   461  				Name: "Unknown",
   462  			},
   463  		},
   464  		{
   465  			name: "Struct input",
   466  			input: struct {
   467  				ID   int    `json:"id"`
   468  				Name string `json:"name"`
   469  			}{ID: 4, Name: "Jane"},
   470  			expected: User{
   471  				ID:   4,
   472  				Name: "Jane",
   473  			},
   474  		},
   475  	}
   476  
   477  	for _, tt := range tests {
   478  		t.Run(tt.name, func(t *testing.T) {
   479  			var result User
   480  			err := schema.Unmarshal(&result, tt.input)
   481  			require.NoError(t, err)
   482  			assert.Equal(t, tt.expected.ID, result.ID)
   483  			assert.Equal(t, tt.expected.Name, result.Name)
   484  		})
   485  	}
   486  }
   487  
   488  // TestUnmarshalNonObjectTypes tests non-object JSON types
   489  func TestUnmarshalNonObjectTypes(t *testing.T) {
   490  	tests := []struct {
   491  		name       string
   492  		schemaJSON string
   493  		input      interface{}
   494  		expected   interface{}
   495  	}{
   496  		{
   497  			name:       "Array schema with JSON bytes",
   498  			schemaJSON: `{"type": "array", "items": {"type": "integer"}}`,
   499  			input:      []byte(`[1, 2, 3]`),
   500  			expected:   []int{1, 2, 3},
   501  		},
   502  		{
   503  			name:       "Number schema with JSON bytes",
   504  			schemaJSON: `{"type": "number", "minimum": 0}`,
   505  			input:      []byte(`42.5`),
   506  			expected:   42.5,
   507  		},
   508  		{
   509  			name:       "Boolean schema",
   510  			schemaJSON: `{"type": "boolean"}`,
   511  			input:      []byte(`true`),
   512  			expected:   true,
   513  		},
   514  		{
   515  			name:       "String schema with plain string",
   516  			schemaJSON: `{"type": "string", "minLength": 3}`,
   517  			input:      "hello world",
   518  			expected:   "hello world",
   519  		},
   520  	}
   521  
   522  	compiler := NewCompiler()
   523  
   524  	for _, tt := range tests {
   525  		t.Run(tt.name, func(t *testing.T) {
   526  			schema, err := compiler.Compile([]byte(tt.schemaJSON))
   527  			require.NoError(t, err)
   528  
   529  			switch tt.expected.(type) {
   530  			case []int:
   531  				var result []int
   532  				err = schema.Unmarshal(&result, tt.input)
   533  				require.NoError(t, err)
   534  				assert.Equal(t, tt.expected, result)
   535  			case string:
   536  				var result string
   537  				err = schema.Unmarshal(&result, tt.input)
   538  				require.NoError(t, err)
   539  				assert.Equal(t, tt.expected, result)
   540  			case float64:
   541  				var result float64
   542  				err = schema.Unmarshal(&result, tt.input)
   543  				require.NoError(t, err)
   544  				assert.Equal(t, tt.expected, result)
   545  			case bool:
   546  				var result bool
   547  				err = schema.Unmarshal(&result, tt.input)
   548  				require.NoError(t, err)
   549  				assert.Equal(t, tt.expected, result)
   550  			}
   551  		})
   552  	}
   553  }
   554  
   555  // TestUnmarshalDefaults tests default value application
   556  func TestUnmarshalDefaults(t *testing.T) {
   557  	type User struct {
   558  		Name    string `json:"name"`
   559  		Age     int    `json:"age"`
   560  		Country string `json:"country"`
   561  		Active  bool   `json:"active"`
   562  		Role    string `json:"role"`
   563  	}
   564  
   565  	schemaJSON := `{
   566  		"type": "object",
   567  		"properties": {
   568  			"name": {"type": "string"},
   569  			"age": {"type": "integer", "minimum": 0},
   570  			"country": {"type": "string", "default": "US"},
   571  			"active": {"type": "boolean", "default": true},
   572  			"role": {"type": "string", "default": "user"}
   573  		},
   574  		"required": ["name", "age"]
   575  	}`
   576  
   577  	tests := []struct {
   578  		name         string
   579  		src          interface{}
   580  		expectError  bool
   581  		expectedUser User
   582  	}{
   583  		{
   584  			name:        "JSON bytes with defaults",
   585  			src:         []byte(`{"name": "John", "age": 25}`),
   586  			expectError: false,
   587  			expectedUser: User{
   588  				Name:    "John",
   589  				Age:     25,
   590  				Country: "US",
   591  				Active:  true,
   592  				Role:    "user",
   593  			},
   594  		},
   595  		{
   596  			name:        "map with defaults",
   597  			src:         map[string]interface{}{"name": "Jane", "age": 30, "country": "CA"},
   598  			expectError: false,
   599  			expectedUser: User{
   600  				Name:    "Jane",
   601  				Age:     30,
   602  				Country: "CA",
   603  				Active:  true,
   604  				Role:    "user",
   605  			},
   606  		},
   607  		{
   608  			name:        "missing required field - no error in unmarshal (validation should be done separately)",
   609  			src:         []byte(`{"age": 25}`),
   610  			expectError: false,
   611  			expectedUser: User{
   612  				Name:    "", // Missing required field, but unmarshal still works
   613  				Age:     25,
   614  				Country: "US",
   615  				Active:  true,
   616  				Role:    "user",
   617  			},
   618  		},
   619  	}
   620  
   621  	for _, tt := range tests {
   622  		t.Run(tt.name, func(t *testing.T) {
   623  			compiler := NewCompiler()
   624  			schema, err := compiler.Compile([]byte(schemaJSON))
   625  			require.NoError(t, err)
   626  
   627  			var result User
   628  			err = schema.Unmarshal(&result, tt.src)
   629  
   630  			if tt.expectError {
   631  				require.Error(t, err)
   632  			} else {
   633  				require.NoError(t, err)
   634  				assert.Equal(t, tt.expectedUser, result)
   635  			}
   636  		})
   637  	}
   638  }
   639  
   640  // BenchmarkUnmarshal tests performance
   641  func BenchmarkUnmarshal(b *testing.B) {
   642  	schemaJSON := `{
   643  		"type": "object",
   644  		"properties": {
   645  			"id": {"type": "integer"},
   646  			"name": {"type": "string", "default": "Test"}
   647  		}
   648  	}`
   649  
   650  	compiler := NewCompiler()
   651  	schema, _ := compiler.Compile([]byte(schemaJSON))
   652  	input := []byte(`{"id": 1}`)
   653  
   654  	b.ResetTimer()
   655  	for i := 0; i < b.N; i++ {
   656  		var result User
   657  		_ = schema.Unmarshal(&result, input)
   658  	}
   659  }
   660  
   661  // Helper function to parse time strings for tests
   662  func parseTime(timeStr string) time.Time {
   663  	t, err := time.Parse(time.RFC3339, timeStr)
   664  	if err != nil {
   665  		panic(err)
   666  	}
   667  	return t
   668  }