Add Bun.JSONC API for parsing JSON with comments and trailing commas (#22115)

## Summary

This PR implements a new `Bun.JSONC.parse()` API that allows parsing
JSONC (JSON with Comments) files. It addresses the feature request from
issue #16257 by providing a native API for parsing JSON with comments
and trailing commas.

The implementation follows the same pattern as `Bun.YAML` and
`Bun.TOML`, leveraging the existing `TSConfigParser` which already
handles JSONC parsing internally.

## Features

- **Parse JSON with comments**: Supports both `//` single-line and `/*
*/` block comments
- **Handle trailing commas**: Works with trailing commas in objects and
arrays
- **Full JavaScript object conversion**: Returns native JavaScript
objects/arrays
- **Error handling**: Proper error throwing for invalid JSON
- **TypeScript compatibility**: Works with TypeScript config files and
other JSONC formats

## Usage Example

```javascript
const result = Bun.JSONC.parse(`{
  // This is a comment
  "name": "my-app",
  "version": "1.0.0", // trailing comma is allowed
  "dependencies": {
    "react": "^18.0.0",
  },
}`);
// Returns native JavaScript object
```

## Implementation Details

- Created `JSONCObject.zig` following the same pattern as
`YAMLObject.zig` and `TOMLObject.zig`
- Uses the existing `TSConfigParser` from `json.zig` which already
handles comments and trailing commas
- Added proper C++ bindings and exports following Bun's established
patterns
- Comprehensive test suite covering various JSONC features

## Test Plan

- [x] Basic JSON parsing works
- [x] Single-line comments (`//`) are handled correctly
- [x] Block comments (`/* */`) are handled correctly  
- [x] Trailing commas in objects and arrays work
- [x] Complex nested structures parse correctly
- [x] Error handling for invalid JSON
- [x] Empty objects and arrays work
- [x] Boolean and null values work correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
robobun
2026-01-08 13:27:47 -08:00
committed by GitHub
parent 65d006aae0
commit eeef013365
7 changed files with 217 additions and 2 deletions

View File

@@ -625,6 +625,33 @@ declare module "bun" {
export function parse(input: string): object;
}
/**
* JSONC related APIs
*/
namespace JSONC {
/**
* Parse a JSONC (JSON with Comments) string into a JavaScript value.
*
* Supports both single-line (`//`) and block comments (`/* ... *\/`), as well as
* trailing commas in objects and arrays.
*
* @category Utilities
*
* @param input The JSONC string to parse
* @returns A JavaScript value
*
* @example
* ```js
* const result = Bun.JSONC.parse(`{
* // This is a comment
* "name": "my-app",
* "version": "1.0.0", // trailing comma is allowed
* }`);
* ```
*/
export function parse(input: string): unknown;
}
/**
* YAML related APIs
*/

View File

@@ -26,8 +26,9 @@ pub const SocketHandlers = @import("./api/bun/socket.zig").Handlers;
pub const Subprocess = @import("./api/bun/subprocess.zig");
pub const Terminal = @import("./api/bun/Terminal.zig");
pub const HashObject = @import("./api/HashObject.zig");
pub const UnsafeObject = @import("./api/UnsafeObject.zig");
pub const JSONCObject = @import("./api/JSONCObject.zig");
pub const TOMLObject = @import("./api/TOMLObject.zig");
pub const UnsafeObject = @import("./api/UnsafeObject.zig");
pub const YAMLObject = @import("./api/YAMLObject.zig");
pub const Timer = @import("./api/Timer.zig");
pub const FFIObject = @import("./api/FFIObject.zig");

View File

@@ -62,6 +62,7 @@ pub const BunObject = struct {
pub const SHA384 = toJSLazyPropertyCallback(Crypto.SHA384.getter);
pub const SHA512 = toJSLazyPropertyCallback(Crypto.SHA512.getter);
pub const SHA512_256 = toJSLazyPropertyCallback(Crypto.SHA512_256.getter);
pub const JSONC = toJSLazyPropertyCallback(Bun.getJSONCObject);
pub const TOML = toJSLazyPropertyCallback(Bun.getTOMLObject);
pub const YAML = toJSLazyPropertyCallback(Bun.getYAMLObject);
pub const Transpiler = toJSLazyPropertyCallback(Bun.getTranspilerConstructor);
@@ -127,7 +128,7 @@ pub const BunObject = struct {
@export(&BunObject.SHA384, .{ .name = lazyPropertyCallbackName("SHA384") });
@export(&BunObject.SHA512, .{ .name = lazyPropertyCallbackName("SHA512") });
@export(&BunObject.SHA512_256, .{ .name = lazyPropertyCallbackName("SHA512_256") });
@export(&BunObject.JSONC, .{ .name = lazyPropertyCallbackName("JSONC") });
@export(&BunObject.TOML, .{ .name = lazyPropertyCallbackName("TOML") });
@export(&BunObject.YAML, .{ .name = lazyPropertyCallbackName("YAML") });
@export(&BunObject.Glob, .{ .name = lazyPropertyCallbackName("Glob") });
@@ -1261,6 +1262,9 @@ pub fn getHashObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSVa
return HashObject.create(globalThis);
}
pub fn getJSONCObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
return JSONCObject.create(globalThis);
}
pub fn getTOMLObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
return TOMLObject.create(globalThis);
}
@@ -2058,6 +2062,7 @@ const gen = bun.gen.BunObject;
const api = bun.api;
const FFIObject = bun.api.FFIObject;
const HashObject = bun.api.HashObject;
const JSONCObject = bun.api.JSONCObject;
const TOMLObject = bun.api.TOMLObject;
const UnsafeObject = bun.api.UnsafeObject;
const YAMLObject = bun.api.YAMLObject;

View File

@@ -0,0 +1,55 @@
pub fn create(globalThis: *jsc.JSGlobalObject) jsc.JSValue {
const object = JSValue.createEmptyObject(globalThis, 1);
object.put(
globalThis,
ZigString.static("parse"),
jsc.JSFunction.create(
globalThis,
"parse",
parse,
1,
.{},
),
);
return object;
}
pub fn parse(
globalThis: *jsc.JSGlobalObject,
callframe: *jsc.CallFrame,
) bun.JSError!jsc.JSValue {
var arena = bun.ArenaAllocator.init(globalThis.allocator());
const allocator = arena.allocator();
defer arena.deinit();
var log = logger.Log.init(default_allocator);
defer log.deinit();
const input_value = callframe.argument(0);
if (input_value.isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArguments("Expected a string to parse", .{});
}
var input_slice = try input_value.toSlice(globalThis, bun.default_allocator);
defer input_slice.deinit();
const source = &logger.Source.initPathString("input.jsonc", input_slice.slice());
const parse_result = json.parseTSConfig(source, &log, allocator, true) catch {
return globalThis.throwValue(try log.toJS(globalThis, default_allocator, "Failed to parse JSONC"));
};
return parse_result.toJS(allocator, globalThis) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.JSError => return error.JSError,
error.JSTerminated => return error.JSTerminated,
// JSONC parsing does not produce macros or identifiers
else => unreachable,
};
}
const bun = @import("bun");
const default_allocator = bun.default_allocator;
const logger = bun.logger;
const json = bun.interchange.json;
const jsc = bun.jsc;
const JSValue = jsc.JSValue;
const ZigString = jsc.ZigString;

View File

@@ -8,6 +8,7 @@
macro(FFI) \
macro(FileSystemRouter) \
macro(Glob) \
macro(JSONC) \
macro(MD4) \
macro(MD5) \
macro(S3Client) \

View File

@@ -727,6 +727,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
SHA384 BunObject_lazyPropCb_wrap_SHA384 DontDelete|PropertyCallback
SHA512 BunObject_lazyPropCb_wrap_SHA512 DontDelete|PropertyCallback
SHA512_256 BunObject_lazyPropCb_wrap_SHA512_256 DontDelete|PropertyCallback
JSONC BunObject_lazyPropCb_wrap_JSONC DontDelete|PropertyCallback
TOML BunObject_lazyPropCb_wrap_TOML DontDelete|PropertyCallback
YAML BunObject_lazyPropCb_wrap_YAML DontDelete|PropertyCallback
Transpiler BunObject_lazyPropCb_wrap_Transpiler DontDelete|PropertyCallback

View File

@@ -0,0 +1,125 @@
import { expect, test } from "bun:test";
test("Bun.JSONC exists", () => {
expect(Bun.JSONC).toBeDefined();
expect(typeof Bun.JSONC).toBe("object");
expect(typeof Bun.JSONC.parse).toBe("function");
});
test("Bun.JSONC.parse handles basic JSON", () => {
const result = Bun.JSONC.parse('{"name": "test", "value": 42}');
expect(result).toEqual({ name: "test", value: 42 });
});
test("Bun.JSONC.parse handles comments", () => {
const jsonc = `{
// This is a comment
"name": "test",
/* This is a block comment */
"value": 42
}`;
const result = Bun.JSONC.parse(jsonc);
expect(result).toEqual({ name: "test", value: 42 });
});
test("Bun.JSONC.parse handles trailing commas", () => {
const jsonc = `{
"name": "test",
"value": 42,
}`;
const result = Bun.JSONC.parse(jsonc);
expect(result).toEqual({ name: "test", value: 42 });
});
test("Bun.JSONC.parse handles arrays with trailing commas", () => {
const jsonc = `[
1,
2,
3,
]`;
const result = Bun.JSONC.parse(jsonc);
expect(result).toEqual([1, 2, 3]);
});
test("Bun.JSONC.parse handles complex JSONC", () => {
const jsonc = `{
// Configuration object
"name": "my-app",
"version": "1.0.0",
/* Dependencies section */
"dependencies": {
"react": "^18.0.0",
"typescript": "^5.0.0", // Latest TypeScript
},
"scripts": [
"build",
"test",
"lint", // Code formatting
],
}`;
const result = Bun.JSONC.parse(jsonc);
expect(result).toEqual({
name: "my-app",
version: "1.0.0",
dependencies: {
react: "^18.0.0",
typescript: "^5.0.0",
},
scripts: ["build", "test", "lint"],
});
});
test("Bun.JSONC.parse handles nested objects", () => {
const jsonc = `{
"outer": {
// Nested comment
"inner": {
"value": 123,
}
},
}`;
const result = Bun.JSONC.parse(jsonc);
expect(result).toEqual({
outer: {
inner: {
value: 123,
},
},
});
});
test("Bun.JSONC.parse handles boolean and null values", () => {
const jsonc = `{
"enabled": true, // Boolean true
"disabled": false, // Boolean false
"nothing": null, // Null value
}`;
const result = Bun.JSONC.parse(jsonc);
expect(result).toEqual({
enabled: true,
disabled: false,
nothing: null,
});
});
test("Bun.JSONC.parse throws on invalid JSON", () => {
expect(() => {
Bun.JSONC.parse("{ invalid json }");
}).toThrow();
});
test("Bun.JSONC.parse handles empty object", () => {
const result = Bun.JSONC.parse("{}");
expect(result).toEqual({});
});
test("Bun.JSONC.parse handles empty array", () => {
const result = Bun.JSONC.parse("[]");
expect(result).toEqual([]);
});