Roadmap #
This document outlines planned features for zig-json, organized by priority. Each feature links to its corresponding GitLab issue for detailed discussion and tracking.
Priority Levels #
- Critical: Core functionality gaps that significantly limit library usage
- High: Important features for production-ready status
- Medium: Enhancements that improve flexibility and developer experience
- Low: Nice-to-have features and edge case handling
Completed Features #
Planned Features #
All planned features have been completed. The library is feature-complete.
Feature Details #
#7 - JSON to Zig Type Unmarshaling/Decoding #
Status: Completed
Unmarshal/decode JSON directly into native Zig types.
API:
// Unmarshal JSON bytes directly to a Zig type
pub fn unmarshal(comptime T: type, allocator: Allocator, input: []const u8) !T
// Unmarshal from a json.Value tree
pub fn fromValue(comptime T: type, allocator: Allocator, value: Value) !T
// Unmarshal with detailed error info
pub fn unmarshalWithInfo(comptime T: type, allocator: Allocator, input: []const u8) UnmarshalResult(T)
Features:
- Primitive type decoding (bool, integers, floats, strings)
- Optional type handling
- Struct field mapping with comptime reflection
- Array/slice decoding
- Enum and tagged union decoding
#8 - Streaming JSON Tokenizer #
Status: Completed
Low-level streaming tokenizer for memory-efficient parsing.
API:
pub const Tokenizer = struct {
pub fn init(input: []const u8, options: TokenizerOptions) Tokenizer;
pub fn next(self: *Tokenizer) !?Token;
};
#9 - Custom Marshaler/Unmarshaler Interface #
Status: Completed
Types can customize JSON serialization/deserialization via jsonMarshal and jsonUnmarshal methods.
Example:
const CustomDate = struct {
year: u16, month: u8, day: u8,
pub fn jsonMarshal(self: CustomDate, allocator: Allocator) !json.Value {
var buf: [10]u8 = undefined;
const str = std.fmt.bufPrint(&buf, "{d:0>4}-{d:0>2}-{d:0>2}",
.{self.year, self.month, self.day}) catch unreachable;
return .{ .str = try allocator.dupe(u8, str) };
}
};
#10 - Struct Field Tag Support #
Status: Completed
Customize JSON field names and behavior per struct field via json_config.
Example:
const Person = struct {
first_name: []const u8,
age: ?u32,
pub const json_config = .{
.first_name = FieldConfig{ .name = "firstName" },
.age = FieldConfig{ .omitempty = true },
};
};
Supported Options:
name- Custom field nameomitempty- Omit if empty/zeroskip- Skip field entirelystringify- Encode numbers as stringsinline- Flatten nested structunknown- Capture unknown fieldsbinary_format- Binary encoding format
#11 - Time and Duration Support #
Status: Completed
First-class time types with automatic marshaling.
Types:
Timestamp- RFC 3339 timestamps ("2024-01-15T10:30:00Z")UnixTimestamp- Unix seconds (numbers)UnixTimestampMs- Unix milliseconds (numbers)Duration- ISO 8601 durations ("PT2H30M")
#12 - Binary Data Format Options #
Status: Completed
Multiple encoding formats for []u8 fields.
Formats:
pub const BinaryFormat = enum {
utf8, // Default - UTF-8 string
base64, // RFC 4648 standard
base64url, // RFC 4648 URL-safe
hex, // Lowercase hexadecimal
hex_upper, // Uppercase hexadecimal
base32, // RFC 4648
};
#13 - Comprehensive Options System #
Status: Completed
Fine-grained control over encoding and unmarshaling.
EncodeOptions:
indent- Pretty-print indentationdeterministic- Sort keys alphabeticallystringify_numbers- Encode all numbers as stringsemit_null_optional_fields- Emit null for optional fieldsescape_non_ascii- Escape non-ASCII charactersescape_slash- Escape forward slashesdefault_binary_format- Default binary encodingallow_nan_infinity- Encode NaN/Infinity as stringsformat_nil_slice_as_null- Empty slices as nulluse_fmt_formatter- Use format() method for types
UnmarshalOptions:
ignore_unknown_fields- Silently ignore unknown fieldsuse_field_tags- Use json_config for field mappingdefault_binary_format- Default binary decodingallow_nan_infinity- Parse "NaN"/"Infinity" stringsmatch_case_insensitive- Case-insensitive field matching
#14 - Inline/Flatten Struct Fields #
Status: Completed
Promote nested struct fields to the parent level.
Example:
const User = struct {
id: u64,
metadata: Metadata,
pub const json_config = .{
.metadata = FieldConfig{ .inline = true },
};
};
// Encodes to: {"id":1,"created_at":123,"updated_at":456}
// Instead of: {"id":1,"metadata":{"created_at":123,...}}
#15 - Unknown Member Capture #
Status: Completed
Capture unknown JSON fields into a designated struct field.
Example:
const Config = struct {
name: []const u8,
extra: []json.Member, // Captures unknown fields
pub const json_config = .{
.extra = FieldConfig{ .unknown = true },
};
};
#16 - Enhanced Error Types #
Status: Completed
Rich semantic errors with JSON Pointer paths.
Types:
pub const UnmarshalErrorInfo = struct {
path: []const u8, // e.g., "/users/0/email"
kind: UnmarshalErrorKind,
expected: ?[]const u8,
found: ?[]const u8,
message: []const u8,
};
pub const UnmarshalErrorKind = enum {
type_mismatch,
missing_field,
unknown_field,
invalid_format,
integer_overflow,
duplicate_field,
json_parse,
};
#17 - NaN and Infinity Handling #
Status: Completed
Encode/decode NaN and Infinity as JSON strings.
Example:
const options = EncodeOptions{ .allow_nan_infinity = true };
// NaN -> "NaN", Infinity -> "Infinity", -Infinity -> "-Infinity"
const unmarshal_opts = UnmarshalOptions{ .allow_nan_infinity = true };
// "NaN" -> NaN, "Infinity" -> Infinity
#18 - std.fmt.Formatter Support #
Status: Completed
Use Zig's format() method for custom string encoding.
Example:
const Point = struct {
x: i32, y: i32,
pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
try writer.print("({d}, {d})", .{ self.x, self.y });
}
};
// With use_fmt_formatter = true:
// Point{.x=10, .y=20} encodes as "(10, 20)"
#19 - Streaming Encoder API #
Status: Completed
Build JSON output incrementally without constructing a Value tree first.
API:
pub fn StreamingEncoder(comptime WriterType: type) type;
pub fn streamingEncoder(allocator: Allocator, writer: anytype, options: EncoderOptions) StreamingEncoder(@TypeOf(writer));
Example:
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(allocator);
var enc = json.streamingEncoder(allocator, buf.writer(allocator), .{});
defer enc.deinit();
try enc.beginObject();
try enc.writeKey("name");
try enc.writeString("Alice");
try enc.writeKey("age");
try enc.writeInt(@as(u32, 30));
try enc.endObject();
// Result: {"name":"Alice","age":30}
Methods:
beginObject()/endObject()- Start/end JSON objectbeginArray()/endArray()- Start/end JSON arraywriteKey(key)- Write object keywriteNull(),writeBool(val),writeString(val)- Write primitiveswriteInt(val),writeFloat(val)- Write numberswriteValue(json.Value)- Write an existing Value treewriteRaw(json_string)- Write pre-formatted JSON
#20 - Streaming Value Reader #
Status: Completed
Read JSON values directly from tokenizer without building full tree first.
API:
pub fn readValue(tokenizer: *Tokenizer) !json.Value;
pub fn freeValue(allocator: Allocator, value: json.Value) void;
Example:
var tokenizer = json.Tokenizer.init(allocator, input, .{});
defer tokenizer.deinit();
// Read just the values you need
const value = try json.readValue(&tokenizer);
defer json.freeValue(allocator, value);
// Process value...
#21 - StringHashMap Unknown Capture #
Status: Completed
Use std.StringHashMap(json.Value) as an alternative to []json.Member for capturing unknown fields.
Example:
const Config = struct {
name: []const u8,
extra: std.StringHashMap(json.Value), // Alternative to []json.Member
pub const json_config = .{
.extra = FieldConfig{ .unknown = true },
};
};
Benefits:
- O(1) lookup for captured fields by key
- More convenient for accessing specific unknown fields
- Same round-trip behavior as []json.Member
#22 - JSON5 Support #
Status: Completed
Parse JSON5, an extension of JSON with additional syntax features.
Features:
- Single-line (
//) and multi-line (/* */) comments - Trailing commas in objects and arrays
- Unquoted object keys (ECMAScript identifiers)
- Single-quoted strings with additional escape sequences
- Hexadecimal numbers (
0xFF) - Leading decimal point (
.5=0.5) - Trailing decimal point (
5.=5.0) - Positive sign prefix (
+5) - NaN and Infinity literals
Usage:
var err = json.ParseError.init();
var doc = try json.parse(allocator, input, .{ .json5 = true }, &err);
#23 - RFC 6902 JSON Patch #
Status: Completed
Apply and parse JSON Patch documents for modifying JSON values.
Operations:
add- Add a value at the target pathremove- Remove the value at the target pathreplace- Replace the value at the target pathmove- Move a value from one location to anothercopy- Copy a value from one location to anothertest- Test that a value equals an expected value
API:
// Apply a patch
const result = try json.applyPatch(allocator, doc, &ops);
defer json.freePatchValue(allocator, result);
// Parse a patch document
const ops = try json.parsePatch(allocator, patch_value);
#24 - RFC 7396 JSON Merge Patch #
Status: Completed
Apply JSON Merge Patch for simpler partial updates.
Rules:
- If patch is not an object, it replaces the target entirely
- For each key in the patch:
- If value is
null, remove the key from target - Otherwise, recursively merge
- If value is
API:
const result = try json.applyMergePatch(allocator, target, patch);
defer json.freePatchValue(allocator, result);
#25 - JSON Lines (JSONL) Parsing #
Status: Completed
Parse newline-delimited JSON (NDJSON/JSON Lines) format.
API:
// Parse all lines at once
var err: json.JsonlError = undefined;
var doc = try json.parseJsonl(allocator, input, .{}, &err);
defer doc.deinit();
// doc.root is an array of parsed values
// Streaming iterator for large files
var iter = json.jsonlIterator(allocator, input, .{});
while (try iter.next(&err)) |*d| {
var doc = d.*;
defer doc.deinit();
// Process each value...
}
#26 - Schema Generation from Zig Types #
Status: Completed
Generate JSON Schema (draft 2020-12) from Zig type definitions.
Supported Types:
- Structs → object schema with properties
- Enums → string enum schema
- Optionals → anyOf with null
- Arrays/slices → array schema with items
- Unions → oneOf schema
- Primitives → appropriate type schemas
API:
const Person = struct {
name: []const u8,
age: u32,
email: ?[]const u8 = null,
};
const schema = try json.generateSchema(Person, allocator, .{});
defer json.freeSchema(allocator, schema);
// Produces: {"type":"object","properties":{"name":{"type":"string"},...}}
#27 - Custom Format Validators #
Status: Completed
Register custom format validators for JSON Schema validation.
API:
fn validatePhoneNumber(str: []const u8) bool {
return std.mem.startsWith(u8, str, "+") and str.len >= 10;
}
const custom_formats = [_]json.FormatValidator{
.{ .name = "phone", .validate = validatePhoneNumber },
};
const result = try json.validate(allocator, &schema, doc.root, .{
.custom_formats = &custom_formats,
});
#28 - JSONPath Query Expressions #
Status: Completed
Query JSON documents using JSONPath syntax (Stefan Goessner specification).
Supported Operators:
$- Root element.nameor['name']- Child element..- Recursive descent*- Wildcard (all children)[n]- Array index (positive and negative)[start:end:step]- Array slice[?(@.field == value)]- Filter expression
API:
const result = try json.jsonpath(allocator, doc.root, "$.users[*].name");
defer result.deinit();
for (result.values) |value| {
std.debug.print("{s}\n", .{value.str});
}
#29 - Custom Time Formats #
Status: Completed
User-defined format strings for time types beyond RFC 3339/ISO 8601.
Supported Format Specifiers:
%Y- 4-digit year%y- 2-digit year%m- 2-digit month%d- 2-digit day%H- 24-hour hour%I- 12-hour hour%M- Minute%S- Second%f- Microseconds%p- AM/PM%z- Timezone offset%Z- Timezone name
API:
const SqlTimestamp = json.FormattedTimestamp(json.TimeFormat.sql_datetime);
// Parse
const ts = try SqlTimestamp.parse("2024-01-15 10:30:00");
// Format
const s = try ts.format(allocator);
defer allocator.free(s);
// Result: "2024-01-15 10:30:00"
Built-in Formats:
TimeFormat.rfc3339- RFC 3339 (2024-01-15T10:30:00Z)TimeFormat.sql_datetime- SQL format (2024-01-15 10:30:00)TimeFormat.us_date- US date (01/15/2024)TimeFormat.eu_date- European date (15/01/2024)TimeFormat.iso8601_date- ISO date (2024-01-15)
#30 - Reader-based Streaming Tokenizer #
Status: Completed
Tokenize JSON from any std.io.Reader source without loading the entire input into memory.
API:
var file = try std.fs.cwd().openFile("large.json", .{});
defer file.close();
var tokenizer = try json.readerTokenizer(allocator, file.reader(), .{});
defer tokenizer.deinit();
while (true) {
const token = try tokenizer.next();
if (token.type == .eof) break;
// Process token...
}
// Or read a complete value:
const value = try json.readValueFromReader(&tokenizer);
defer json.freeValue(allocator, value);
#31 - JSON Diff Generation (RFC 6902) #
Status: Completed
Generate JSON Patch documents that transform one JSON value into another.
API:
// Generate diff
const ops = try json.diffPatch(allocator, from, to);
defer json.freeDiffOperations(allocator, ops);
// Apply the diff to verify
const result = try json.applyPatch(allocator, from, ops);
defer json.freePatchValue(allocator, result);
// Serialize to JSON
const patch_json = try json.patchToValue(allocator, ops);
defer json.freePatchValue(allocator, patch_json);
#32 - Schema $ref Resolution Caching #
Status: Completed
Improved performance for JSON Schema validation through caching of resolved $ref and $dynamicRef references.
Benefits:
- Avoids repeated URI parsing and resolution during validation
- Significant speedup for schemas with many references
- Automatic cache invalidation per validation session
- No API changes required - optimization is transparent
#33 - Standalone SIMD Scanning Primitives #
Status: Completed (opt-in primitives only)
Vectorized JSON scanning helpers built on Zig's @Vector types. These are
standalone, tested primitives available for opt-in use. They are not wired
into the library's default parser, tokenizer, or unmarshaler, which remain
scalar. The primitives operate on raw byte slices and do not, on their own,
perform JSON validation, escape decoding, or surrogate-pair handling.
Primitives:
- Whitespace skipping using SIMD vector comparisons
- Structural character detection (braces, brackets, quotes, colons, commas)
- String boundary scanning with quote/backslash detection
- UTF-8 validation (SIMD ASCII fast path with scalar fallback)
- Batch helpers for counting structurals and locating string spans
API:
const simd = json.simd_mod;
// Batch helpers
var processor = simd.BatchProcessor.init(allocator);
// Locate string spans: returns []{ start, end }
const spans = try processor.findAllStrings(input);
defer allocator.free(spans);
// Count structural characters
const counts = processor.countStructurals(input);
// counts.objects, counts.arrays, counts.strings, counts.colons, counts.commas
// Skip leading whitespace (returns index of first non-whitespace byte)
const first = simd.skipWhitespaceSIMD(input);
// Validate UTF-8
const valid = simd.validateUtf8SIMD(input);
#34 - Async Parsing Support #
Status: Completed
Non-blocking incremental JSON parsing for async contexts with progress reporting.
Features:
- Incremental parsing with configurable yield intervals
- Progress callbacks and cancellation support
- Memory-efficient streaming from any source
- Resume parsing after yielding
API:
const async_parser = json.async_parser;
// Create parser
var parser = async_parser.AsyncParser.init(allocator, .{
.yield_interval = 4096, // Yield every 4KB
.max_depth = 256,
});
defer parser.deinit();
// Feed data incrementally
try parser.feed(chunk1);
try parser.feed(chunk2);
// Parse with yielding
while (true) {
const result = parser.parse();
switch (result) {
.yield => |progress| {
// Report progress, check for cancellation
std.debug.print("Progress: {d}%\n", .{progress.percent * 100});
},
.done => |value| {
// Parsing complete
break;
},
.err => |e| {
// Handle error
break;
},
}
}
#35 - JSON-LD Support #
Status: Completed
JSON for Linked Data (JSON-LD) processing including context expansion, compaction, and serialization.
Features:
- Context expansion and compaction (JSON-LD 1.1)
- Document flattening
- N-Quads serialization
- Frame processing
API:
const jsonld = json.jsonld;
// Expand document
const expanded = try jsonld.expand(allocator, document, .{});
// Compact with context
const compacted = try jsonld.compact(allocator, document, context, .{});
// Flatten
const flattened = try jsonld.flatten(allocator, document, context, .{});
// Convert to N-Quads
const nquads = try jsonld.toNQuads(allocator, document, .{});
defer allocator.free(nquads);
#36 - CBOR Encoding (RFC 8949) #
Status: Completed
Concise Binary Object Representation encoding and decoding per RFC 8949.
Features:
- Full major type support (0-7)
- Indefinite-length encoding
- Canonical encoding option
- Tag support (datetime, bignum, etc.)
- Diagnostic notation output
- Streaming encoding
API:
const cbor = json.cbor;
// Encode JSON to CBOR
const bytes = try cbor.encode(allocator, json_value, .{});
defer allocator.free(bytes);
// Decode CBOR to JSON
const value = try cbor.decode(allocator, cbor_bytes, .{});
defer cbor.free(allocator, value);
// Canonical encoding
const canonical = try cbor.encode(allocator, value, .{ .canonical = true });
// Diagnostic notation
const diag = try cbor.toDiagnostic(allocator, cbor_bytes);
defer allocator.free(diag);
// e.g., '{"name": "Alice", "age": 30}'
Supported CBOR Types:
- Unsigned integers (major type 0)
- Negative integers (major type 1)
- Byte strings (major type 2)
- Text strings (major type 3)
- Arrays (major type 4)
- Maps (major type 5)
- Tags (major type 6)
- Simple values and floats (major type 7)
Contributing #
Contributions are welcome! Please check the linked issues for detailed specifications and discussion before starting work on a feature.
- Pick an issue from the list above
- Comment on the issue to indicate you're working on it
- Submit a merge request with your implementation
- Include tests for new functionality
References #
This roadmap is inspired by Go's encoding/json/v2 package design: