JSON Repair — Zig source
Fix broken JSON - trailing commas, single quotes, unquoted keys, comments, Python constants, BOM and truncated documents - and get clean pretty-printed JSON plus a list of every repair applied. Runs entirely in your browser.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! JSON repair pipeline — Zig port of the json-repair tool: the two scanner
//! passes (quote conversion + truncation recovery) of the 7-pass pipeline in
//! typescript.ts / go.go; regex-shaped passes live in the higher-level ports.
const std = @import("std");
const Allocator = std.mem.Allocator;
/// Single-quoted strings -> double-quoted: escape inner double quotes,
/// collapse \' to ', keep every other escape; double-quoted copy verbatim.
fn singleToDoubleQuotes(alloc: Allocator, text: []const u8) Allocator.Error![]u8 {
var out = std.ArrayList(u8).init(alloc);
errdefer out.deinit();
var i: usize = 0;
while (i < text.len) {
if (text[i] == '"') { // copy a double-quoted string verbatim
try out.append('"');
i += 1;
while (i < text.len and text[i] != '"') {
if (text[i] == '\\' and i + 1 < text.len) { try out.append(text[i]); i += 1; }
try out.append(text[i]);
i += 1;
}
if (i < text.len) { try out.append('"'); i += 1; }
} else if (text[i] == '\'') {
try out.append('"');
i += 1;
while (i < text.len and text[i] != '\'') {
if (text[i] == '\\' and i + 1 < text.len) {
if (text[i + 1] == '\'') try out.append('\'') // \' -> '
else try out.appendSlice(text[i .. i + 2]); // other escapes kept
i += 2;
} else if (text[i] == '"') {
try out.appendSlice("\\\"");
i += 1;
} else { try out.append(text[i]); i += 1; }
}
if (i < text.len) i += 1; // skip the closing quote
try out.append('"');
} else { try out.append(text[i]); i += 1; }
}
return out.toOwnedSlice();
}
/// Recover truncated JSON: close an unterminated string, drop a dangling
/// comma, give a dangling colon a null value, then close open brackets.
fn closeTruncated(alloc: Allocator, text: []const u8) Allocator.Error![]u8 {
const stack = try alloc.alloc(u8, text.len + 1);
defer alloc.free(stack);
var top: usize = 0;
var in_string = false;
var i: usize = 0;
while (i < text.len) : (i += 1) {
const c = text[i];
if (in_string) {
if (c == '\\' and i + 1 < text.len) i += 1 else if (c == '"') in_string = false;
} else {
if (c == '"') in_string = true;
if (c == '{' or c == '[') { stack[top] = c; top += 1; }
if (c == '}' or c == ']') { if (top > 0) top -= 1; }
}
}
var out = std.ArrayList(u8).init(alloc);
errdefer out.deinit();
try out.appendSlice(text[0..i]);
if (in_string) try out.append('"');
while (out.items.len > 0 and (out.items[out.items.len - 1] == ',' or std.ascii.isWhitespace(out.items[out.items.len - 1]))) out.items.len -= 1;
if (out.items.len > 0 and out.items[out.items.len - 1] == ':') try out.appendSlice(" null");
while (top > 0) { top -= 1; try out.append(if (stack[top] == '{') '}' else ']'); }
return out.toOwnedSlice();
}
/// The canonical repair() runs all 7 passes and pretty-prints; this excerpt
/// chains the two scanner passes (the arena frees the intermediates).
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const alloc = arena.allocator();
// {"name": "Ada", tags: ["x", "y"]}
const quoted = try singleToDoubleQuotes(alloc, "{'name': 'Ada', tags: ['x', 'y',");
const repaired = try closeTruncated(alloc, quoted);
std.debug.print("{s}\n", .{repaired});
}
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 →