JuliusBrussee/caveman · error

json splice: replacement is not valid JSON

Error message

json splice: replacement is not valid JSON

What it means

ReplaceRaw requires the replacement bytes to already be valid JSON since they are inserted verbatim into the document. If json.Valid(replacement) is false the splice is aborted to avoid corrupting the envelope.

Solutions

  1. Run replacement through json.Marshal (or json.Valid in a unit test) before calling ReplaceRaw
  2. Quote raw strings with json.Marshal(string) so they become valid JSON literals
  3. Check the error of any earlier marshal step instead of ignoring it and passing empty/partial bytes

Example fix

// before: plain string spliced in
repl := []byte(`cache_control`)
out, err := jsonsplice.ReplaceRaw(body, span, repl)
// after: marshal to valid JSON
raw, err := json.Marshal(map[string]any{"cache_control": map[string]string{"type": "ephemeral"}})
if err != nil { return nil, err }
out, err := jsonsplice.ReplaceRaw(body, span, raw)
Defensive patterns

Strategy: validation

Validate before calling

if !json.Valid(repl) { return fmt.Errorf("replacement is not valid JSON: %q", repl) }

Try / catch

raw, err := json.Marshal(value)
if err != nil { return nil, err }
if !json.Valid(raw) { return nil, fmt.Errorf("marshal produced invalid JSON") }
out, err := jsonsplice.ReplaceRaw(body, span, raw)

Prevention

When it happens

Trigger: Passing Go values, plain strings, or partially-serialized buffers as replacement instead of marshaled JSON; hand-built JSON with a syntax error; forgetting json.Marshal on a string value (must be quoted).

Common situations: Injecting a cache_control object where the struct was serialized with a non-JSON encoder; interpolating Go strings directly into the replacement; truncated JSON from a failed marshal earlier in the pipeline.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/283aff289300f8ac. Report an issue: GitHub.

Appendix: source

Thrown at proxy/providers/jsonsplice/jsonsplice.go:142

		out = append(out, body[last:candidate.Start]...)
		out = append(out, quoted...)
		last = candidate.End
	}
	if out == nil {
		return body, nil
	}
	return append(out, body[last:]...), nil
}

// ReplaceRaw replaces one JSON value while preserving every byte outside span.
// replacement must itself be valid JSON. Unlike Replace, it does not quote the
// replacement and is suitable for provider-envelope edits.
func ReplaceRaw(body []byte, span Span, replacement []byte) ([]byte, error) {
	if span.Start < 0 || span.End > len(body) || span.Start >= span.End {
		return nil, fmt.Errorf("json splice: invalid range")
	}
	if !json.Valid(replacement) {
		return nil, fmt.Errorf("json splice: replacement is not valid JSON")
	}
	out := make([]byte, 0, len(body)-(span.End-span.Start)+len(replacement))
	out = append(out, body[:span.Start]...)
	out = append(out, replacement...)
	out = append(out, body[span.End:]...)
	return out, nil
}

// AppendObjectFields inserts fields immediately before an object's closing
// brace. Existing bytes, whitespace, key order, and escapes remain untouched.
// Callers must reject existing decoded keys before calling this function.
func AppendObjectFields(body []byte, object Span, fields ...FieldInsertion) ([]byte, error) {
	if object.Start < 0 || object.End > len(body) || object.Start >= object.End ||
		body[object.Start] != '{' || body[object.End-1] != '}' {
		return nil, fmt.Errorf("json splice: invalid object range")
	}
	if len(fields) == 0 {
		return body, nil

View on GitHub (pinned to 3ee70a1026)