Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

All notable changes to this project will be documented in this file.

## [Unreleased]

### Stdlib

- Added a `@acton/json` module for reading values out of JSON documents during
tests and scripts, complementing `fs.readFile`. Values are addressed with a
JSON Pointer (RFC 6901): `json.getInt`, `json.getString`, and `json.getBool`
return `null` on invalid JSON, a missing path, or a type mismatch (consistent
with `@acton/env`), and `json.exists` reports whether a pointer resolves.

## [1.1.0] - 22.05.2026

Acton 1.1.0 is the first feature release after `v1.0.0`. It focuses on
Expand Down
112 changes: 112 additions & 0 deletions docs/content/docs/standard_library/json.mdx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions docs/content/docs/standard_library/overview.mdx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 68 additions & 0 deletions lib/json.tolk
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/// Module for reading values out of JSON documents.
///
/// This module provides a small, read-only JSON decoder for use during contract
/// testing or script execution. It is the natural companion to `fs.readFile`,
/// which loads a JSON file as a `string` but does not parse it.
///
/// Values are addressed with a JSON Pointer (RFC 6901): a `/`-separated path
/// such as `/token/decimals` or `/items/0`. An empty pointer (`""`) selects the
/// whole document.
///
/// Lookup semantics (consistent with `@acton/env`):
/// - Invalid JSON, a missing path, or a type mismatch all return `null`, so `??`
/// can supply a default.
/// - `getInt` accepts a JSON integer, or a JSON string holding a decimal or
/// `0x`-hex integer. Non-integers (floats) return `null`. To carry full
/// 257-bit values losslessly, encode the number as a JSON string.
///
/// Examples:
/// ```tolk
/// val src = fs.readFile("fixtures/jetton-metadata.json");
/// if (src != null) {
/// val decimals = json.getInt(src!, "/decimals") ?? 9;
/// val name = json.getString(src!, "/name") ?? "unknown";
/// val mintable = json.getBool(src!, "/mintable") ?? false;
/// if (json.exists(src!, "/admin")) {
/// println("metadata declares an admin");
/// }
/// }
/// ```

/// Namespace for JSON decoding operations.
struct json

/// Reads an integer at the given JSON Pointer.
///
/// Returns:
/// - the value when it is a JSON integer, or a JSON string containing a decimal
/// or `0x`-hex integer.
/// - `null` when the source is invalid JSON, the pointer is absent, or the value
/// is not an integer (for example a float, boolean, object, or array).
///
/// Bare JSON numbers larger than 64 bits are not reliably representable; pass
/// such values as JSON strings to preserve full precision.
fun json.getInt(src: string, path: string): int?
asm "300 EXTCALL"

/// Reads a string at the given JSON Pointer.
///
/// Returns the value when it is a JSON string, otherwise `null` (invalid JSON,
/// missing pointer, or a non-string value).
fun json.getString(src: string, path: string): string?
asm "301 EXTCALL"

/// Reads a boolean at the given JSON Pointer.
///
/// Returns the value when it is a JSON boolean, otherwise `null` (invalid JSON,
/// missing pointer, or a non-boolean value). A present `false` is returned as
/// `false`, not `null`.
fun json.getBool(src: string, path: string): bool?
asm "302 EXTCALL"

/// Checks whether the given JSON Pointer resolves to a value.
///
/// Returns:
/// - `true` when the pointer resolves to any JSON value (including `null`).
/// - `false` when the source is invalid JSON or the pointer is absent.
fun json.exists(src: string, path: string): bool
asm "303 EXTCALL"
102 changes: 102 additions & 0 deletions src/ffi/json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use crate::context::Context;
use num_bigint::BigInt;
use serde_json::Value;
use std::str::FromStr;
use ton_emulator::{extension, register_ext_methods};
use ton_executor::BaseExecutor;
use tvm_ffi::stack::{Tuple, TupleItem};

/// Parses `src` as a JSON document and returns the value at the given
/// JSON Pointer (RFC 6901, e.g. `/token/decimals` or `/items/0`).
///
/// Returns `None` when `src` is not valid JSON or when the pointer does not
/// resolve to a value. An empty pointer (`""`) selects the whole document.
fn lookup(src: &str, pointer: &str) -> Option<Value> {
let root: Value = serde_json::from_str(src).ok()?;
root.pointer(pointer).cloned()
}

/// Parses an integer from a JSON string value: decimal, or hexadecimal when
/// prefixed with `0x`/`0X`. Mirrors the parsing accepted by `env<int>`.
fn parse_int_str(raw: &str) -> Option<BigInt> {
let raw = raw.trim();
if let Ok(value) = BigInt::from_str(raw) {
return Some(value);
}
let hex = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X"))?;
BigInt::parse_bytes(hex.as_bytes(), 16)
}

extension!(json_get_int in (Context) with (pointer: String, src: String) using json_get_int_impl);
fn json_get_int_impl(
_ctx: &mut Context,
stack: &mut Tuple,
pointer: String,
src: String,
) -> anyhow::Result<()> {
// NOTE: stack is LIFO, so the host receives the Tolk arguments reversed:
// Tolk `json.getInt(src, pointer)` -> Rust `(pointer, src)`.
let parsed = lookup(&src, &pointer).and_then(|value| match value {
// Bare JSON integers. Floats fail `BigInt::from_str` and yield `null`.
// Values beyond 64-bit should be encoded as JSON strings (see below),
// since serde_json parses oversized bare numbers as lossy floats.
Value::Number(number) => BigInt::from_str(&number.to_string()).ok(),
// Quoted numbers (decimal or 0x-hex). This is the lossless way to carry
// full 257-bit TON integers through JSON.
Value::String(text) => parse_int_str(&text),
_ => None,
});
match parsed {
Some(value) => stack.push(TupleItem::Int(value)),
None => stack.push(TupleItem::Null),
}
Ok(())
}

extension!(json_get_string in (Context) with (pointer: String, src: String) using json_get_string_impl);
fn json_get_string_impl(
_ctx: &mut Context,
stack: &mut Tuple,
pointer: String,
src: String,
) -> anyhow::Result<()> {
match lookup(&src, &pointer) {
Some(Value::String(text)) => stack.push_string(&text),
_ => stack.push(TupleItem::Null),
}
Ok(())
}

extension!(json_get_bool in (Context) with (pointer: String, src: String) using json_get_bool_impl);
fn json_get_bool_impl(
_ctx: &mut Context,
stack: &mut Tuple,
pointer: String,
src: String,
) -> anyhow::Result<()> {
match lookup(&src, &pointer) {
Some(Value::Bool(flag)) => stack.push_bool(flag),
_ => stack.push(TupleItem::Null),
}
Ok(())
}

extension!(json_exists in (Context) with (pointer: String, src: String) using json_exists_impl);
fn json_exists_impl(
_ctx: &mut Context,
stack: &mut Tuple,
pointer: String,
src: String,
) -> anyhow::Result<()> {
stack.push_bool(lookup(&src, &pointer).is_some());
Ok(())
}

pub fn register_extensions<T: BaseExecutor>(executor: &mut T, ctx: &mut Context) {
register_ext_methods!(executor, ctx, {
300 => json_get_int : 2,
301 => json_get_string : 2,
302 => json_get_bool : 2,
303 => json_exists : 2,
});
}
2 changes: 2 additions & 0 deletions src/ffi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod emulation;
pub mod env;
pub mod fs;
pub mod io;
pub mod json;

#[derive(Clone, Copy)]
#[repr(usize)]
Expand Down Expand Up @@ -39,6 +40,7 @@ impl SearchParamIndex {
pub fn register<T: BaseExecutor>(executor: &mut T, ctx: &mut Context) {
io::register_extensions(executor, ctx);
fs::register_extensions(executor, ctx);
json::register_extensions(executor, ctx);
boc::register_extensions(executor, ctx);
env::register_extensions(executor, ctx);
assert::register_extensions(executor, ctx);
Expand Down
Loading