diff --git a/text/0000-json-encode-jsonvalue.md b/text/0000-json-encode-jsonvalue.md new file mode 100644 index 00000000..35152c2a --- /dev/null +++ b/text/0000-json-encode-jsonvalue.md @@ -0,0 +1,87 @@ +- Feature Name: json-encode-jsonvalue +- Start Date: 2026-03-12 +- RFC PR: (leave this empty) +- Pony Issue: (leave this empty) + +# Summary + +Adapt the stdlib [`json` package][json-package] in order to add a `JsonPrinter` primitive exposing functions to encode arbitrary [`JsonValue`][JsonValue]s into `String iso^`, in a similar manner as the existing `JsonParser` for parsing [`JsonValue`][JsonValue] from String. + +# Motivation + +The basic functionality of a json library should be to provide a representation for structured json values (which is [JsonValue][JsonValue]) and methods to decode a string or bytes as JSON into this [JsonValue][JsonValue] and vice versa, to encode all possible representations of [JsonValue][JsonValue] into valid JSON bytes (or string). The current [`json` package][json-package] is lacking the means of encoding all [JsonValue][JsonValue] instances into valid JSON. + +`JsonObject` and `JsonArray` provide methods for encoding them as valid JSON with the `.string()` and `.pretty_string()` methods, but for all other possible JSON values (string, number, bool, null) there is no such method. The `.string()` method on `None` is producing `"None"` (without the quotes), which is invalid JSON. Another example: the String `"\""` is copying itself when `.string()` is called on it, without any escaping, which is also invalid JSON. The method `.pretty_string()` is missing from all those other [JsonValue][JsonValue] instances. + +This package needs a consistent and convenient way to encode and decode json bytes from and to all possible [JsonValue][JsonValue] instances. + +# Detailed design + +This PR suggests adding a new primitive called `JsonPrinter`: + +```pony +primitive JsonPrinter + """ + Serialize any `JsonValue` to a JSON string. + """ + fun print(value: JsonValue): String iso^ => + """Compact JSON serialization of any `JsonValue`.""" + _JsonPrint.compact(value) + + fun pretty(value: JsonValue, indent: String = " "): String iso^ => + """Pretty-printed JSON serialization of any `JsonValue`.""" + _JsonPrint.pretty(value, indent) +``` + +All the bits and pieces for encoding arbitrary values are there already, they just aren't exposed: `_JsonPrint` is private. + +To avoid confusion and misuse, the methods `JsonObject.string()` and `JsonArray.string()` (and `.string_pretty()`) are being renamed to `JsonObject.print()`, `JsonArray.print()` and `JsonObject.pretty_print()`, `JsonArray.pretty_print()`. This has the side-effect of both `JsonObject` and `JsonArray` and thus `JsonValue` not implementing `Stringable` anymore. + +# How We Teach This + +The package documentation of the `json` package should show usage examples of the new `JsonPrinter` functions as prominently as `JsonParser` usage and then show all other components of the crate, like `JsonValue`, `JsonPath` or `JsonNav`. + +The intend of this primitive is to expose an easy-to-use interface for turning JSON values into strings as a dual to `JsonParser`. This should also answer the question of how to serialize Pony objects and data-structures as JSON (build a `JsonValue`, then pass it to `JsonPrinter.print(value)`). + + +# How We Test This + +The [`json` package][json-package] already has print-parse roundtrip tests with arbitrary json values. Those need to be adapted to use the public API. Furthermore several "example"-tests are needed, verifying that printing a concrete `JsonValue` yields an expected valid JSON string (i.e. bools, ints, nulls, floats, strings with values needing escape sequences etc.). Those are there to proof that we don't have a common bug in printing and parsing which makes roundtripping work, but produces invalid JSON. + +# Drawbacks + +Renaming `.string()` and `.string_pretty()` on `JsonObject` and `JsonArray` to `.print()` and `.pretty_print()` is a breaking change. It will cause libraries and applications using the `json` package to be changed in order to be compiled with the ponyc version containing this change. + +# Alternatives + +## Function naming alternatives + +- `parse` / `print` - this would follow the existing naming in the package the closest, hence considered as the best candidate. +- `encode` / `decode` - this pair would make sense if the decoding would not expose `JsonParseError`. It feels inconsistent to create a `ParseError` from a `decode` function call. +- `serialize` / `deserialize` - these terms are not used anywhere yet in pony, so didn't favor them. +- `to_string` / `from_string` - The most humble variant of them all, getting along without any fancy terminology. + +Note that this RFC only includes `print` in scope, so renaming the `parse` would be out of scope unless we decided to pursue one of the above alternatives for a broader change. + +## Removing pretty + +It has also been considered to condense both `pretty` and `print` into only one exposed function `JsonPrinter.print()`, similar to `JsonPrinter.print_with_options()` below. But this makes it cumbersome to enable pretty-printing: All parameters need to be examined and understood to configure a printing mode which is equivalent to what `.pretty()` would do. + +```pony +primitive JsonPrinter + fun print_with_options(value: JsonValue, indent: String = "", add_newlines: Bool = false, add_spaces: Bool = false): String iso^ => + """JSON serialization with all possible formatting options exposed. Defaults to compact formatting.""" + // TBD +``` + +## Exposing verbose `print_with_options` as third method of JsonPrinter + +Another alternative is to expose `print_with_options` above as third function and define `.print()` and `.pretty()` as aliases with arguments provided that are equivalent to their current formatting. +This would allow for greater control for users, if both `.print()` and `.pretty()` formatting do not fit their needs. This was not suggested as the use-case for further control was considered way too exotic. + +# Unresolved questions + +None. + +[json-package]: https://github.com/ponylang/ponyc/tree/main/packages/json +[JsonValue]: https://github.com/ponylang/ponyc/blob/main/packages/json/json.pony#L243