When I started learning Go, my first real task was to merge two JSON files and validate the result. It sounded easy. It turned out to be a great way to learn how Go handles file I/O, JSON, maps, and type safety. Here’s how I did it, what I learned, and some alternatives I found along the way. The Task Read two JSON files. Merge them into a single structure. Validate the result to make sure certain fields exist and are of the correct type. Write the final JSON to a new file. Read two JSON files. Merge them into a single structure. Validate the result to make sure certain fields exist and are of the correct type. Write the final JSON to a new file. Step 1: Reading the JSON Files Go has great support for reading files and decoding JSON using only the standard library. data1, _ := ioutil.ReadFile("file1.json") data2, _ := ioutil.ReadFile("file2.json") var json1 map[string]interface{} var json2 map[string]interface{} json.Unmarshal(data1, &json1) json.Unmarshal(data2, &json2) data1, _ := ioutil.ReadFile("file1.json") data2, _ := ioutil.ReadFile("file2.json") var json1 map[string]interface{} var json2 map[string]interface{} json.Unmarshal(data1, &json1) json.Unmarshal(data2, &json2) Note: In newer versions of Go, you can use os.ReadFile instead of ioutil.ReadFile, since ioutil is deprecated. Note: In newer versions of Go, you can use os.ReadFile instead of ioutil.ReadFile, since ioutil is deprecated. os.ReadFile ioutil.ReadFile ioutil Step 2: Merging the JSON Maps I needed a function to merge two maps. If a key existed in both maps, the value from the second map should overwrite the first. If the value was a nested object, I needed to merge it recursively. Here’s the function I wrote: func mergeMaps(m1, m2 map[string]interface{}) map[string]interface{} { for k, v2 := range m2 { if v1, ok := m1[k]; ok { if v1Map, ok := v1.(map[string]interface{}); ok { if v2Map, ok := v2.(map[string]interface{}); ok { m1[k] = mergeMaps(v1Map, v2Map) continue } } } m1[k] = v2 } return m1 } func mergeMaps(m1, m2 map[string]interface{}) map[string]interface{} { for k, v2 := range m2 { if v1, ok := m1[k]; ok { if v1Map, ok := v1.(map[string]interface{}); ok { if v2Map, ok := v2.(map[string]interface{}); ok { m1[k] = mergeMaps(v1Map, v2Map) continue } } } m1[k] = v2 } return m1 } This handled both flat and nested JSON structures correctly. Step 3: Validating the JSON Next, I wrote a simple validation function to check that certain fields were present and of the expected type. Since I wasn’t using a schema validator, I had to do manual checks. func validateJSON(data map[string]interface{}) error { if _, ok := data["name"].(string); !ok { return fmt.Errorf("missing or invalid 'name' field") } if port, ok := data["port"]; !ok || reflect.TypeOf(port).Kind() != reflect.Float64 { return fmt.Errorf("missing or invalid 'port' field") } return nil } func validateJSON(data map[string]interface{}) error { if _, ok := data["name"].(string); !ok { return fmt.Errorf("missing or invalid 'name' field") } if port, ok := data["port"]; !ok || reflect.TypeOf(port).Kind() != reflect.Float64 { return fmt.Errorf("missing or invalid 'port' field") } return nil } Note: When decoding JSON into interface{}, numbers are treated as float64 by default, which can be confusing at first. interface{} float64 Step 4: Writing the Result Finally, I wrote the merged and validated JSON back to a file using pretty formatting. resultBytes, _ := json.MarshalIndent(merged, "", " ") ioutil.WriteFile("merged_output.json", resultBytes, 0644) resultBytes, _ := json.MarshalIndent(merged, "", " ") ioutil.WriteFile("merged_output.json", resultBytes, 0644) It worked exactly as expected. Alternatives: Using a Package to Merge JSON Later, I discovered some packages make JSON merging easier. These are a few good ones: 1. mergo mergo mergo is a popular library for merging maps and structs in Go. It supports nested merges and lets you choose whether to overwrite values. mergo Install: Install go get github.com/imdario/mergo go get github.com/imdario/mergo Example: Example var a map[string]interface{} var b map[string]interface{} json.Unmarshal([]byte(`{"config": {"debug": false}}`), &a) json.Unmarshal([]byte(`{"config": {"debug": true, "port": 8080}}`), &b) mergo.Merge(&a, b, mergo.WithOverride) var a map[string]interface{} var b map[string]interface{} json.Unmarshal([]byte(`{"config": {"debug": false}}`), &a) json.Unmarshal([]byte(`{"config": {"debug": true, "port": 8080}}`), &b) mergo.Merge(&a, b, mergo.WithOverride) Use this if you want clean, deep merging without writing recursive code. 2. json-patch (go-patch) json-patch json-patch lets you apply JSON Patch operations. It’s more for diffing and applying structured updates, but still useful for working with JSON dynamically. json-patch Install: Install go get github.com/evanphx/json-patch go get github.com/evanphx/json-patch Use this if you’re working with patches or want to programmatically update JSON with control. 3. mapstructure mapstructure mapstructure helps decode dynamic maps into strongly typed structs. It’s not a merge tool by itself but works well after you merge JSON data and want to bind it to a Go struct. mapstructure Install: Install go get github.com/mitchellh/mapstructure go get github.com/mitchellh/mapstructure Use this when you want structure and type safety after parsing JSON. Final Thoughts This task helped me learn a lot about how Go handles data, how JSON works under the hood, and why strong typing matters. Writing a merge function gave me control, but using libraries like mergo made the code more maintainable later. mergo If you're learning Go, I recommend starting with small tasks like this. You’ll get real experience with files, encoding, maps, and error handling, all while solving a practical problem. It’s a small project, but one that taught me a lot.