├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── src ├── array.zig └── lib.zig └── zig.mod /.gitignore: -------------------------------------------------------------------------------- 1 | /zig-cache 2 | /zig-out 3 | .zigmod 4 | deps.zig 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Diego Barria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zjson 2 | 3 | A very tiny json library, it allows you to get a json value from a path. 4 | Inspired by [jsonparser](https://github.com/buger/jsonparser), a Go library. 5 | 6 | This library is useful when you dont want to parse whole JSON file, or when 7 | the structure is to complex to parse to structs. It **allocates no memory**. 8 | 9 | > This library is still WIP, the API might change. There can be some bugs as it's not fully tested. 10 | 11 | ## API usage: 12 | 13 | Here there is a basic example. 14 | 15 | ```zig 16 | const input = 17 | \\ { 18 | \\ "student": [ 19 | \\ { 20 | \\ "id": "01", 21 | \\ "name": "Tom", 22 | \\ "lastname": "Price" 23 | \\ }, 24 | \\ { 25 | \\ "id": "02", 26 | \\ "name": "Nick", 27 | \\ "lastname": "Thameson" 28 | \\ } 29 | \\ ] 30 | \\ } 31 | ; 32 | 33 | const lastname = try get(input, .{ "student", 1, "lastname" }); 34 | // Thameson 35 | 36 | 37 | // Iterates an array 38 | const students = try get(input, .{"student"}); 39 | 40 | var iter = try zjson.ArrayIterator(students); 41 | while(try iter.next()) |s| { 42 | const name = try get(value.bytes, .{"name"}); 43 | std.debug.print("student name: {s}\n", .{name.bytes}); 44 | } 45 | // "student name: Tom" 46 | // "student name: Nick" 47 | ``` 48 | 49 | For more usage examples, you can check [ytmusic-zig](https://github.com/xyaman/ytmusic-zig). 50 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) void { 4 | // Standard release options allow the person running `zig build` to select 5 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 6 | const mode = b.standardReleaseOptions(); 7 | 8 | const lib = b.addStaticLibrary("lib", "src/lib.zig"); 9 | lib.setBuildMode(mode); 10 | lib.install(); 11 | 12 | const main_tests = b.addTest("src/lib.zig"); 13 | main_tests.setBuildMode(mode); 14 | 15 | const test_step = b.step("test", "Run library tests"); 16 | test_step.dependOn(&main_tests.step); 17 | } 18 | -------------------------------------------------------------------------------- /src/array.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const lib = @import("lib.zig"); 4 | const Value = lib.Value; 5 | const Error = lib.Error; 6 | 7 | pub const ArrayIterator = struct { 8 | input: []const u8, 9 | index: usize = 0, 10 | cursor: usize = 1, // we want to skip '[' 11 | 12 | const Self = @This(); 13 | 14 | pub fn init(value: Value) !ArrayIterator { 15 | if (value.kind != .array) { 16 | return error.InvalidValue; 17 | } 18 | 19 | return ArrayIterator{ .input = value.bytes }; 20 | } 21 | 22 | pub fn next(self: *Self) Error!?Value { 23 | self.cursor += try lib.find_next_char(self.input[self.cursor..]); 24 | if (self.cursor == self.input.len or self.input[self.cursor] == ']') { 25 | return null; 26 | } 27 | 28 | const value = try lib.get(self.input[self.cursor..], .{}); 29 | self.cursor += value.offset + value.bytes.len + 1; 30 | 31 | // find, 32 | self.cursor += try lib.find_next_char(self.input[self.cursor..]); 33 | if (self.input[self.cursor] == ',') { 34 | self.index += 1; 35 | } 36 | 37 | return value; 38 | } 39 | }; 40 | 41 | test "readme with array iterator" { 42 | const input = 43 | \\ { 44 | \\ "student": [ 45 | \\ { 46 | \\ "id": "01", 47 | \\ "name": "Tom", 48 | \\ "lastname": "Price" 49 | \\ }, 50 | \\ { 51 | \\ "id": "02", 52 | \\ "name": "Nick", 53 | \\ "lastname": "Thameson" 54 | \\ } 55 | \\ ] 56 | \\ } 57 | ; 58 | 59 | const students = try lib.get(input, .{"student"}); 60 | var iter = try ArrayIterator.init(students); 61 | 62 | while (try iter.next()) |s| { 63 | const name = lib.get(s.bytes, .{"name"}) catch unreachable; 64 | std.debug.print("student name: {s}\n", .{name.bytes}); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const ArrayIterator = @import("array.zig").ArrayIterator; 4 | 5 | const PathItem = union(enum) { 6 | key: []const u8, 7 | index: usize, 8 | }; 9 | 10 | /// Represents possible errors in this library. 11 | pub const Error = error{ 12 | InvalidJSON, 13 | InvalidBlock, 14 | KeyNotFound, 15 | IndexNotFound, 16 | InvalidTypeCast, 17 | UnexpectedEOF, 18 | }; 19 | 20 | /// Represents a JSON value 21 | pub const Value = struct { 22 | bytes: []const u8, 23 | kind: ValueKind, 24 | offset: usize, 25 | 26 | const Self = @This(); 27 | 28 | pub fn toBool(self: Self) Error!bool { 29 | if (std.mem.eql(u8, self.bytes, "true")) { 30 | return true; 31 | } else if (std.mem.eql(u8, self.bytes, "false")) { 32 | return false; 33 | } 34 | 35 | return Error.NotABoolean; 36 | } 37 | }; 38 | 39 | /// Represents a JSON value type 40 | pub const ValueKind = enum { 41 | boolean, 42 | string, 43 | integer, 44 | float, 45 | object, 46 | array, 47 | @"null", // TODO: implement null 48 | }; 49 | 50 | /// Represents a JSON block, such as 51 | /// Array, Object, String 52 | const Block = struct { 53 | offset: usize, 54 | bytes: []const u8, 55 | }; 56 | 57 | /// Returns offset of the next significant char 58 | pub fn find_next_char(input: []const u8) !usize { 59 | var offset: usize = 0; 60 | 61 | // advances until finds a valid char 62 | while (offset < input.len) { 63 | switch (input[offset]) { 64 | ' ', '\n', '\r', '\t' => { 65 | offset += 1; 66 | }, 67 | 68 | else => return offset, 69 | } 70 | } 71 | 72 | return 0; 73 | } 74 | 75 | /// Returns a json `Value` based on a path. 76 | pub fn get(input: []const u8, path: anytype) Error!Value { 77 | 78 | // basically we get the path fields, and save as `PathItem` 79 | const path_fields = comptime std.meta.fields(@TypeOf(path)); 80 | 81 | var keys: [path_fields.len]PathItem = undefined; 82 | inline for (path_fields) |f, i| { 83 | switch (f.field_type) { 84 | // used for array indexes 85 | comptime_int => { 86 | keys[i] = PathItem{ .index = f.default_value.? }; 87 | }, 88 | 89 | // everything else is a string for us, the compiler should handle 90 | // this errors, until I found a better way. 91 | // it is pretty? NO 92 | // does it work for now? YES 93 | else => { 94 | keys[i] = PathItem{ .key = f.default_value.? }; 95 | }, 96 | } 97 | } 98 | 99 | // reads input until match path, or until it ends 100 | var cursor: usize = 0; 101 | var key_matched = keys.len == 0; // this way we can run the function without path, useful to get data type 102 | var depth: usize = 0; 103 | 104 | while (cursor < input.len) { 105 | cursor += try find_next_char(input[cursor..]); 106 | 107 | // std.log.warn("depth: {d} -> {s}", .{ depth, input[0 .. cursor + 1] }); 108 | 109 | switch (input[cursor]) { 110 | '{' => { 111 | // we want to skip the block if its not matched, 112 | // unless its on the first level 113 | if (!key_matched and depth > 0) { 114 | const block = try read_block(input[cursor..], '{', '}'); 115 | cursor += block.offset + 1; // we add 1 to include '}' 116 | key_matched = false; 117 | continue; 118 | } 119 | 120 | // we are at the end, so we return current block 121 | if (key_matched and depth == keys.len) { 122 | const block = try read_block(input[cursor..], '{', '}'); 123 | return Value{ 124 | .bytes = block.bytes, 125 | .kind = .object, 126 | .offset = cursor, 127 | }; 128 | } 129 | 130 | // if we are here, it means the last key was matched or we are 131 | // just starting with the parsing so its safe to increase level 132 | // and cursor 133 | key_matched = false; 134 | depth += 1; 135 | cursor += 1; 136 | }, 137 | '[' => { 138 | // we want to skip the block if its not matched, 139 | // unless its on the first level 140 | if (!key_matched and depth > 0) { 141 | const block = try read_block(input[cursor..], '[', ']'); 142 | cursor += block.offset + 1; // we add 1 to include ']' 143 | continue; 144 | } 145 | 146 | // we are at the end, so we return current block 147 | if (key_matched and depth == keys.len) { 148 | const block = try read_block(input[cursor..], '[', ']'); 149 | return Value{ 150 | .bytes = block.bytes, 151 | .kind = .array, 152 | .offset = cursor, 153 | }; 154 | } 155 | 156 | // Probably used to get type, like object or array 157 | if (keys.len == 0) { 158 | const block = try read_block(input[cursor..], '[', ']'); 159 | return Value{ 160 | .bytes = block.bytes, 161 | .kind = .array, 162 | .offset = cursor, 163 | }; 164 | } 165 | 166 | // if we are here, it means the last key was matched or we are 167 | // just starting with the parsing so its safe to increase level 168 | // and cursor 169 | if (key_matched) { 170 | depth += 1; 171 | 172 | const index = switch (keys[depth - 1]) { 173 | .index => |v| v, 174 | else => return error.KeyNotFound, 175 | }; 176 | 177 | const item = try get_offset_by_index(input[cursor..], index); 178 | cursor += item; 179 | continue; 180 | } 181 | 182 | key_matched = false; 183 | cursor += 1; 184 | }, 185 | 186 | '"' => { 187 | // parse double quote block 188 | const value = try read_string(input[cursor..]); 189 | 190 | // it means we are at the end 191 | if (key_matched and depth == keys.len) { 192 | return Value{ 193 | .bytes = value.bytes, 194 | .kind = .string, 195 | .offset = cursor, 196 | }; 197 | } 198 | 199 | cursor += value.offset + 1; 200 | 201 | const next_cursor = try find_next_char(input[cursor..]); 202 | 203 | // if (input[cursor + next_cursor] == ':' and keys.len > 0) not works according the compiler 204 | if (input[cursor + next_cursor] == ':') { 205 | if (keys.len > 0) { 206 | cursor += next_cursor; 207 | 208 | // here only keys works 209 | const key = switch (keys[depth - 1]) { 210 | .key => |v| v, 211 | else => return Error.KeyNotFound, 212 | }; 213 | 214 | // compare key with corresponding key in path param 215 | if (std.mem.eql(u8, value.bytes, key)) { 216 | key_matched = true; 217 | } 218 | } 219 | } 220 | }, 221 | 222 | // number 223 | '-', '0'...'9' => { 224 | const number = read_number(input[cursor..]); 225 | if (key_matched) { 226 | return Value{ 227 | .bytes = number.inner, 228 | .kind = number.kind, 229 | .offset = cursor, 230 | }; 231 | } 232 | 233 | cursor += number.offset; 234 | }, 235 | 236 | // boolean 237 | 't' => { 238 | const offset = (try read_until(input[cursor..], 'e')) + 1; 239 | const is_valid = std.mem.eql(u8, input[cursor .. cursor + offset], "true"); 240 | 241 | if (is_valid) { 242 | if (key_matched) { 243 | return Value{ 244 | .bytes = input[cursor .. offset + cursor], 245 | .kind = .boolean, 246 | .offset = cursor + offset, 247 | }; 248 | } 249 | cursor += offset; 250 | continue; 251 | } 252 | 253 | return Error.InvalidJSON; 254 | }, 255 | 'f' => { 256 | const offset = (try read_until(input[cursor..], 'e')) + 1; 257 | const is_valid = std.mem.eql(u8, input[cursor .. cursor + offset], "false"); 258 | 259 | if (is_valid) { 260 | if (key_matched) { 261 | return Value{ 262 | .bytes = input[cursor .. offset + cursor], 263 | .kind = .boolean, 264 | .offset = cursor + offset, 265 | }; 266 | } 267 | cursor += offset; 268 | continue; 269 | } 270 | 271 | return Error.InvalidJSON; 272 | }, 273 | 'n' => { 274 | // not pretty 275 | var offset: usize = undefined; 276 | if (cursor + 4 < input.len) { 277 | offset = 4; 278 | } else { 279 | return Error.InvalidJSON; 280 | } 281 | 282 | const is_valid = std.mem.eql(u8, input[cursor .. cursor + offset], "null"); 283 | 284 | if (is_valid) { 285 | if (key_matched) { 286 | return Value{ 287 | .bytes = input[cursor .. offset + cursor], 288 | .kind = .@"null", 289 | .offset = cursor + offset, 290 | }; 291 | } 292 | cursor += offset; 293 | continue; 294 | } 295 | 296 | return Error.InvalidJSON; 297 | }, 298 | else => cursor += 1, 299 | } 300 | } 301 | 302 | return Error.InvalidJSON; 303 | } 304 | 305 | fn read_block(input: []const u8, start: u8, end: u8) Error!Block { 306 | 307 | // first we should start block, is a valid block 308 | // this should never happens i guess 309 | if (input[0] != start) { 310 | @panic("library error when parsing block, this is my fault"); 311 | } 312 | 313 | var offset: usize = 1; 314 | 315 | // now we read until find close delimiter 316 | while (offset < input.len and input[offset] != end) { 317 | if (input[offset] == start) { 318 | var block = try read_block(input[offset..], start, end); 319 | offset += block.offset; 320 | } 321 | 322 | offset += 1; 323 | } 324 | 325 | // if we reach the end and we didnt find the end delimiter, 326 | // it means the block is invalid, because it has no end. 327 | if (offset == input.len) { 328 | return Error.InvalidBlock; 329 | } 330 | 331 | return Block{ .offset = offset, .bytes = input[0 .. offset + 1] }; 332 | } 333 | 334 | fn read_string(input: []const u8) Error!Block { 335 | 336 | // first we should start block, is a valid block 337 | // this should never happens i guess 338 | if (input[0] != '"') { 339 | @panic("library error when parsing string, this is my fault"); 340 | } 341 | 342 | var cursor: usize = 1; 343 | var last_escaped = false; 344 | 345 | // now we read until find close delimiter 346 | while (cursor < input.len) { 347 | switch (input[cursor]) { 348 | '"' => { 349 | if (last_escaped) { 350 | cursor += 1; 351 | last_escaped = false; 352 | } else { 353 | break; 354 | } 355 | }, 356 | '\\' => last_escaped = true, 357 | else => last_escaped = false, 358 | } 359 | cursor += 1; 360 | } 361 | 362 | // if we reach the end and we didnt find the end delimiter, 363 | // it means the block is invalid, because it has no end. 364 | if (cursor == input.len) { 365 | return Error.InvalidBlock; 366 | } 367 | 368 | return Block{ .offset = cursor, .bytes = input[1..cursor] }; 369 | } 370 | 371 | // has a lot of errors for now, specially on detecting format errors, 372 | // ex: -8.65.4 373 | // 8-5.6 374 | // both valid numbers acording this function, but obviously not 375 | fn read_number(input: []const u8) struct { offset: usize, inner: []const u8, kind: ValueKind } { 376 | var offset: usize = 0; 377 | var kind: ValueKind = .integer; 378 | 379 | // now we read until find close delimeter 380 | while (std.ascii.isDigit(input[offset]) or input[offset] == '.' or input[offset] == '-') : (offset += 1) { 381 | if (input[offset] == '-') { 382 | kind = .float; 383 | } 384 | } 385 | 386 | return .{ .offset = offset, .inner = input[0..offset], .kind = kind }; 387 | } 388 | 389 | // Returns the offset, between the start of input to delimeter 390 | fn read_until(input: []const u8, delimiter: u8) Error!usize { 391 | var offset: usize = 0; 392 | 393 | while (offset < input.len and input[offset] != delimiter) { 394 | offset += 1; 395 | } 396 | 397 | return offset; 398 | } 399 | 400 | // input needs to be a json array in this format: 401 | // "[a, b, c, d, ...]" 402 | // 403 | // Returns the offset from 0 to the previous byte 404 | fn get_offset_by_index(input: []const u8, index: usize) Error!usize { 405 | var offset: usize = 0; 406 | 407 | // check if is an array 408 | if (input[offset] == '[') { 409 | offset += 1; 410 | // else return error 411 | } 412 | 413 | var cursor_index: usize = 0; 414 | while (offset < input.len) { 415 | offset += try find_next_char(input[offset..]); 416 | 417 | if (cursor_index == index) { 418 | return offset; 419 | } 420 | 421 | switch (input[offset]) { 422 | '[', '{', '"', '-', '0'...'9', 't', 'f' => { 423 | const value = try get(input[offset..], .{}); 424 | offset += value.offset + value.bytes.len; 425 | }, 426 | // always at the end 427 | ']' => { 428 | offset += 1; 429 | }, 430 | 431 | ',' => { 432 | offset += 1; 433 | cursor_index += 1; 434 | }, 435 | else => @panic("Not supported yet"), 436 | } 437 | } 438 | 439 | return Error.IndexNotFound; 440 | } 441 | 442 | test " " { 443 | std.testing.refAllDecls(@This()); 444 | } 445 | 446 | test "one level, only string" { 447 | var input = 448 | \\ { 449 | \\ "key1": "value1", 450 | \\ "key2": "value2", 451 | \\ "key3": false 452 | \\ } 453 | ; 454 | 455 | var value1 = try get(input, .{"key1"}); 456 | try std.testing.expect(std.mem.eql(u8, value1.bytes, "value1")); 457 | 458 | var value2 = try get(input, .{"key2"}); 459 | try std.testing.expect(std.mem.eql(u8, value2.bytes, "value2")); 460 | 461 | var value3 = try get(input, .{"key3"}); 462 | try std.testing.expect(std.mem.eql(u8, value3.bytes, "false")); 463 | } 464 | 465 | test "one level, only integer" { 466 | var input = 467 | \\ { 468 | \\ "key1": 8, 469 | \\ "key2": 5654 470 | \\ } 471 | ; 472 | 473 | var value1 = try get(input, .{"key1"}); 474 | try std.testing.expect(std.mem.eql(u8, value1.bytes, "8")); 475 | 476 | var value2 = try get(input, .{"key2"}); 477 | try std.testing.expect(std.mem.eql(u8, value2.bytes, "5654")); 478 | } 479 | 480 | test "two level, only string" { 481 | var input = 482 | \\ { 483 | \\ "key1": { "key11": "value11" }, 484 | \\ "key2": "value2", 485 | \\ "key3": { "key11": "value11" } 486 | \\ } 487 | ; 488 | 489 | var value1 = try get(input, .{ "key1", "key11" }); 490 | try std.testing.expect(std.mem.eql(u8, value1.bytes, "value11")); 491 | 492 | var value2 = try get(input, .{"key2"}); 493 | try std.testing.expect(std.mem.eql(u8, value2.bytes, "value2")); 494 | 495 | var value3 = try get(input, .{"key3"}); 496 | try std.testing.expect(std.mem.eql(u8, value3.bytes, "{ \"key11\": \"value11\" }")); 497 | } 498 | 499 | test "array" { 500 | var input = 501 | \\ { 502 | \\ "key1": [8, 5, 6], 503 | \\ "key2": 5654 504 | \\ } 505 | ; 506 | 507 | var value1 = try get(input, .{ "key1", 2 }); 508 | try std.testing.expect(std.mem.eql(u8, value1.bytes, "6")); 509 | } 510 | 511 | test "array with more keys" { 512 | var input = 513 | \\ { 514 | \\ "key1": [8, {"foo": "bar", "value": 6}, 5], 515 | \\ "key2": [{"foo": 23} , {"foo": "bar", "value": 6}, 5], 516 | \\ "key3": 5654 517 | \\ } 518 | ; 519 | 520 | var value1 = try get(input, .{ "key1", 1, "value" }); 521 | try std.testing.expect(std.mem.eql(u8, value1.bytes, "6")); 522 | } 523 | 524 | test "array with more keys 2" { 525 | var input = 526 | \\ { 527 | \\ "key": [ 528 | \\ { 529 | \\ "value": 6, 530 | \\ }, 531 | \\ { 532 | \\ "value": 8 533 | \\ "value2": null 534 | \\ } 535 | \\ ] 536 | \\ } 537 | ; 538 | 539 | var value1 = try get(input, .{ "key", 1, "value" }); 540 | try std.testing.expect(std.mem.eql(u8, value1.bytes, "8")); 541 | var value2 = try get(input, .{ "key", 1, "value2" }); 542 | try std.testing.expect(value2.kind == .@"null"); 543 | } 544 | -------------------------------------------------------------------------------- /zig.mod: -------------------------------------------------------------------------------- 1 | id: l64s8atidrsikqlyicxj8afma5wioue5i7ogitqvenkyyqrn 2 | name: zjson 3 | main: src/lib.zig 4 | license: MIT 5 | description: Minimal json library with zero allocations. 6 | dependencies: 7 | --------------------------------------------------------------------------------