JSON Validator — Go source
Validate JSON and pinpoint errors with line and column numbers. Clear valid/invalid verdict plus the exact error location - runs entirely in your browser.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package jsonvalidator is the Go twin of CosmoDev's src/lib/jsonValidate.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in
// lock-step). Pure + deterministic, never panics. The table-driven tests in
// json-validator_test.go share vectors with src/lib/jsonValidate.test.ts so
// the two implementations are held to the same contract.
//
// Position recovery is engine-dependent, exactly as in the TS lib:
//
// - The TS lib runs JSON.parse (V8), which embeds the error offset in its
// SyntaxError message as "at position N"; LocateError/ExtractOffset read
// it back out of the message string. JavaScriptCore (bun) omits it, in
// which case line/column gracefully fall back to null.
// - Go's encoding/json instead reports the offset on the typed error
// (*json.SyntaxError.Offset, a byte offset into the input). ValidateJson
// reads that typed offset directly and feeds it to the same OffsetToLineCol
// helper the message-based path uses. The exported message-based helpers
// (ExtractOffset / LocateError) are preserved verbatim for callers that
// already hold an engine message.
//
// Note: src/lib/jsonValidate.ts does NOT use WebCrypto or any hashing — it is
// pure JSON parsing — so this twin needs no crypto; stdlib only.
package jsonvalidator
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
// ValidationResult mirrors the ValidationResult interface in jsonValidate.ts.
// Error, Line, and Column are pointers so a nil value represents the TS null
// (e.g. a valid parse yields Error=nil, Line=nil, Column=nil).
type ValidationResult struct {
Valid bool
Error *string // nil when valid (TS: error: null)
Line *int // nil when no position is available (TS: line: null)
Column *int // nil when no position is available (TS: column: null)
}
// offsetRe matches V8's "at position N" — the same regex as extractOffset() in
// jsonValidate.ts. JavaScriptCore omits the position, in which case there is no
// match and recovery returns null.
var offsetRe = regexp.MustCompile(`at position (\d+)`)
// jscPrefixRe strips JavaScriptCore's "JSON Parse error: " prefix — the same
// regex (case-insensitive, anchored) as normalizeError() in jsonValidate.ts.
var jscPrefixRe = regexp.MustCompile(`(?i)^JSON Parse error:\s*`)
// strptr returns a pointer to s; intptr returns a pointer to i. They build the
// nullable fields of ValidationResult and the LocateError return values.
func strptr(s string) *string { return &s }
func intptr(i int) *int { return &i }
// ExtractOffset reads the 0-based char offset from a SyntaxError message (V8:
// "at position N"). It is the Go twin of extractOffset() in jsonValidate.ts.
// The bool is false (mirroring TS null) when no position is present.
func ExtractOffset(message string) (int, bool) {
m := offsetRe.FindStringSubmatch(message)
if m == nil {
return 0, false
}
n, err := strconv.Atoi(m[1])
if err != nil {
return 0, false
}
return n, true
}
// ErrorMessage coerces a caught value into a human-readable message. It is the
// Go twin of errorMessage() in jsonValidate.ts: an error yields its message,
// nil yields "null", and anything else is formatted with %v (TS String(value)).
func ErrorMessage(value any) string {
if err, ok := value.(error); ok {
return err.Error()
}
if value == nil {
return "null"
}
return fmt.Sprintf("%v", value)
}
// NormalizeError strips JavaScriptCore's "JSON Parse error: " prefix for
// display. It is the Go twin of normalizeError() in jsonValidate.ts; V8-style
// messages are returned untouched.
func NormalizeError(message string) string {
return jscPrefixRe.ReplaceAllString(message, "")
}
// OffsetToLineCol converts a 0-based char offset into a 1-based {line, column}.
// It is the Go twin of offsetToLineCol() in jsonValidate.ts: '\n', a lone '\r',
// and '\r\n' each count as a single line break, and an offset past the end is
// clamped to the text length. Bytes are iterated (mirroring the TS char indexing);
// for the contract's ASCII inputs a byte index equals a code-point index.
func OffsetToLineCol(text string, offset int) (line, column int) {
line, column = 1, 1
max := min(offset, len(text)) // clamp an offset past the end to the text length
for i := 0; i < max; i++ {
switch text[i] {
case '\n':
line++
column = 1
case '\r':
line++
column = 1
if i+1 < len(text) && text[i+1] == '\n' {
i++ // treat CRLF as a single break
}
default:
column++
}
}
return line, column
}
// LocateError maps a parse error message to a {line, column} (or nils) using
// the input text. It is the Go twin of locateError() in jsonValidate.ts: the
// offset is read from the message via ExtractOffset, and nil is returned for
// both when the message carries no position.
func LocateError(text, message string) (line, column *int) {
offset, ok := ExtractOffset(message)
if !ok {
return nil, nil
}
l, c := OffsetToLineCol(text, offset)
return intptr(l), intptr(c)
}
// locateSyntaxError recovers a 1-based {line, column} from a Go JSON parse
// error. encoding/json reports the offset on the typed *json.SyntaxError (a
// byte offset into the input) rather than in the message, so we read it
// directly and feed it to OffsetToLineCol. Returns nils when the error carries
// no offset, mirroring the TS lib's null fallback for engines that omit it.
func locateSyntaxError(text string, err error) (line, column *int) {
var se *json.SyntaxError
if errors.As(err, &se) {
l, c := OffsetToLineCol(text, int(se.Offset))
return intptr(l), intptr(c)
}
return nil, nil
}
// ValidateJson validates a JSON string. It is the Go twin of validateJson() in
// jsonValidate.ts and never panics:
// - empty / whitespace-only input → invalid, "Input is empty", no location
// - valid JSON → Valid: true, nil error/line/column
// - invalid JSON → Valid: false, a cleaned error message,
// and a 1-based line/column when the
// engine reports an offset (else nil)
func ValidateJson(text string) ValidationResult {
if strings.TrimSpace(text) == "" {
return ValidationResult{Valid: false, Error: strptr("Input is empty")}
}
var v any
if err := json.Unmarshal([]byte(text), &v); err != nil {
line, col := locateSyntaxError(text, err)
return ValidationResult{
Valid: false,
Error: strptr(NormalizeError(ErrorMessage(err))),
Line: line,
Column: col,
}
}
return ValidationResult{Valid: true}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →