← Home

Getting line and character positions from Go’s JSON unmarshal errors

It’s really easy to map JSON to structs in Go using the json.Unmarshal function, but the default error message doesn’t include information which would help to fix the input file, such as the line and character position in the input file.

I’d guess this is because it would be a performance hit to make the JSON parser count line breaks as its going along or something.

Unmarshalling invalid JSON looks something like this:

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	validJSON := `{ 
		"key": "value" 
	}`
	invalidJSON := `{
		"key": "unclosedQuote
	}`
	mismatchedTypeJSON := `{
	
		"key": 123
	}`

	for i, j := range []string{validJSON, invalidJSON, mismatchedTypeJSON} {
		var d data
		err := json.Unmarshal([]byte(j), &d)
		if err != nil {
			fmt.Printf("test %d failed with error: %v\n", i+1, err)
			continue
		}
		fmt.Printf("test %d passed\n", i+1)
	}
}

type data struct {
	Key string `json:"key"`
}

You can run it at [0]

The output, showing the error messages is:

test 1 passed
test 2 failed with error: invalid character '\n' in string literal
test 3 failed with error: json: cannot unmarshal number into Go struct field data.key of type string

The snippet demonstrates the two main types of JSON unmarshalling errors.

JSON Syntax Errors

Situations where the JSON is invalid because of a missing curly brace or quote mark etc.

The implementation of the error interface returned by Go in this case is [1]

JSON Type Errors

This is returned when the JSON type (string, number, array) can’t be converted to the Go type (e.g. string, int32, float64) on a struct because the two are incompatible.

In this case, the error is of type [2]

Getting more information

The problem with the snippet above is that it just prints out the results of the Error() function, while the actual error types returned by the json.Unmarshal function contain more information, including the offset (in bytes) of the error. In some cases, we’d want to give that information to the end user to help them understand what’s going wrong.

This second version retrieves the extra data and uses a function I wrote to get the line and character position where the error occurred from the offset, which helps greatly if you’re troubleshooting problems.

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

func main() {
	validJSON := `{ 
		"key": "value" 
	}`
	invalidJSON := `{
		"key": "unclosedQuote
	}`
	mismatchedTypeJSON := `{
	
		"key": 123
	}`

	for i, j := range []string{validJSON, invalidJSON, mismatchedTypeJSON} {
		var d data
		err := json.Unmarshal([]byte(j), &d)
		if jsonError, ok := err.(*json.SyntaxError); ok {
			line, character, lcErr := lineAndCharacter(j, int(jsonError.Offset))
			fmt.Fprintf(os.Stderr, "test %d failed with error: Cannot parse JSON schema due to a syntax error at line %d, character %d: %v\n", i+1, line, character, jsonError.Error())
			if lcErr != nil {
				fmt.Fprintf(os.Stderr, "Couldn't find the line and character position of the error due to error %v\n", lcErr)
			}
			continue
		}
		if jsonError, ok := err.(*json.UnmarshalTypeError); ok {
			line, character, lcErr := lineAndCharacter(j, int(jsonError.Offset))
			fmt.Fprintf(os.Stderr, "test %d failed with error: The JSON type '%v' cannot be converted into the Go '%v' type on struct '%s', field '%v'. See input file line %d, character %d\n", i+1, jsonError.Value, jsonError.Type.Name(), jsonError.Struct, jsonError.Field, line, character)
			if lcErr != nil {
				fmt.Fprintf(os.Stderr, "test %d failed with error: Couldn't find the line and character position of the error due to error %v\n", i+1, lcErr)
			}
			continue
		}
		if err != nil {
			fmt.Fprintf(os.Stderr, "test %d failed with error: %v\n", i+1, err)
			continue
		}
		fmt.Printf("test %d passed\n", i+1)
	}
}

func lineAndCharacter(input string, offset int) (line int, character int, err error) {
	lf := rune(0x0A)

	if offset > len(input) || offset < 0 {
		return 0, 0, fmt.Errorf("Couldn't find offset %d within the input.", offset)
	}

	// Humans tend to count from 1.
	line = 1

	for i, b := range input {
		if b == lf {
			line++
			character = 0
		}
		character++
		if i == offset {
			break
		}
	}

	return line, character, nil
}

type data struct {
	Key string `json:"key"`
}

It can be run from [3]

The output this time is more detailed:

test 1 passed
test 2 failed with error: Cannot parse JSON schema due to a syntax error at line 2, character 2: invalid character '\n' in string literal
test 3 failed with error: The JSON type 'number' cannot be converted into the Go 'string' type on struct 'data', field 'key'. See input file line 3, character 1