diff --git a/CHANGELOG.md b/CHANGELOG.md
index a97be43845..2cf5490b33 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/docs/content/docs/standard_library/json.mdx b/docs/content/docs/standard_library/json.mdx
new file mode 100644
index 0000000000..e31585978e
--- /dev/null
+++ b/docs/content/docs/standard_library/json.mdx
@@ -0,0 +1,112 @@
+---
+title: "json"
+description: "json.tolk standard library file"
+---
+
+import { SourceCodeLink } from '@/components/SourceCodeLink';
+
+{/* @generated by `acton docgen`. Do not edit directly. */}
+{/* Source: `lib/json.tolk` */}
+
+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");
+ }
+}
+```
+
+## Definitions
+
+## `json`
+
+```tolk
+struct json
+```
+
+Namespace for JSON decoding operations.
+
+
+
+## `json.getInt`
+
+```tolk
+fun json.getInt(src: string, path: string): int?
+```
+
+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.
+
+
+
+## `json.getString`
+
+```tolk
+fun json.getString(src: string, path: string): string?
+```
+
+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).
+
+
+
+## `json.getBool`
+
+```tolk
+fun json.getBool(src: string, path: string): bool?
+```
+
+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`.
+
+
+
+## `json.exists`
+
+```tolk
+fun json.exists(src: string, path: string): bool
+```
+
+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.
+
+
+
diff --git a/docs/content/docs/standard_library/overview.mdx b/docs/content/docs/standard_library/overview.mdx
index a04630989e..d383d37033 100644
--- a/docs/content/docs/standard_library/overview.mdx
+++ b/docs/content/docs/standard_library/overview.mdx
@@ -47,6 +47,9 @@ The Tolk stdlib is documented in [Tolk standard library](/docs/tolk_standard_lib
Module for input/output operations.
+
+ Module for reading values out of JSON documents.
+
Module for user prompts.
diff --git a/lib/json.tolk b/lib/json.tolk
new file mode 100644
index 0000000000..eb9f1b8f5f
--- /dev/null
+++ b/lib/json.tolk
@@ -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"
diff --git a/src/ffi/json.rs b/src/ffi/json.rs
new file mode 100644
index 0000000000..ba45249233
--- /dev/null
+++ b/src/ffi/json.rs
@@ -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 {
+ 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`.
+fn parse_int_str(raw: &str) -> Option {
+ 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(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,
+ });
+}
diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs
index ec254d9925..3bbd3a2c8f 100644
--- a/src/ffi/mod.rs
+++ b/src/ffi/mod.rs
@@ -9,6 +9,7 @@ pub mod emulation;
pub mod env;
pub mod fs;
pub mod io;
+pub mod json;
#[derive(Clone, Copy)]
#[repr(usize)]
@@ -39,6 +40,7 @@ impl SearchParamIndex {
pub fn register(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);
diff --git a/tests/integration/test_runner/json_reads_typed_values_by_pointer_tests.rs b/tests/integration/test_runner/json_reads_typed_values_by_pointer_tests.rs
new file mode 100644
index 0000000000..83e466b3d4
--- /dev/null
+++ b/tests/integration/test_runner/json_reads_typed_values_by_pointer_tests.rs
@@ -0,0 +1,114 @@
+use crate::support::TestOutputExt;
+use crate::support::project::ProjectBuilder;
+
+const METADATA_JSON: &str = r#"{
+ "name": "Demo",
+ "decimals": 9,
+ "mintable": true,
+ "frozen": false,
+ "big": "123456789012345678901234567890",
+ "hex": "0x1a",
+ "ratio": 1.5,
+ "token": { "symbol": "DEMO" },
+ "items": [10, 20, 30]
+}"#;
+
+const JSON_IMPORTS: &str = r#"
+import "../../lib/fs"
+import "../../lib/json"
+import "../../lib/testing/expect"
+"#;
+
+fn run_case(project_name: &str, test_body: &str) {
+ let test_code = format!("{JSON_IMPORTS}\n{test_body}\n");
+
+ ProjectBuilder::new(project_name)
+ .test_file("json_behavior", &test_code)
+ .raw_file("fixtures/metadata.json", METADATA_JSON)
+ .build()
+ .acton()
+ .test()
+ .run()
+ .success()
+ .assert_passed(1);
+}
+
+#[test]
+fn json_get_int_reads_numbers_strings_and_hex_and_rejects_floats() {
+ run_case(
+ "z-stdlib-json-get-int",
+ r#"
+get fun `test z stdlib json get int`() {
+ val src = fs.readFile("fixtures/metadata.json")!;
+
+ expect(json.getInt(src, "/decimals")).toEqual(9);
+ expect(json.getInt(src, "/items/1")).toEqual(20);
+ expect(json.getInt(src, "/hex")).toEqual(26);
+ expect(json.getInt(src, "/big")).toEqual(123456789012345678901234567890);
+
+ expect(json.getInt(src, "/ratio")).toBeNull();
+ expect(json.getInt(src, "/name")).toBeNull();
+ expect(json.getInt(src, "/missing")).toBeNull();
+}
+"#,
+ );
+}
+
+#[test]
+fn json_get_string_reads_strings_and_nested_paths() {
+ run_case(
+ "z-stdlib-json-get-string",
+ r#"
+get fun `test z stdlib json get string`() {
+ val src = fs.readFile("fixtures/metadata.json")!;
+
+ expect(json.getString(src, "/name")!).toEqual("Demo");
+ expect(json.getString(src, "/token/symbol")!).toEqual("DEMO");
+
+ expect(json.getString(src, "/decimals")).toBeNull();
+ expect(json.getString(src, "/missing")).toBeNull();
+}
+"#,
+ );
+}
+
+#[test]
+fn json_get_bool_distinguishes_present_false_from_null() {
+ run_case(
+ "z-stdlib-json-get-bool",
+ r#"
+get fun `test z stdlib json get bool`() {
+ val src = fs.readFile("fixtures/metadata.json")!;
+
+ expect(json.getBool(src, "/mintable")!).toEqual(true);
+
+ val frozen = json.getBool(src, "/frozen");
+ expect(frozen).toBeNotNull();
+ expect(frozen!).toEqual(false);
+
+ expect(json.getBool(src, "/decimals")).toBeNull();
+ expect(json.getBool(src, "/missing")).toBeNull();
+}
+"#,
+ );
+}
+
+#[test]
+fn json_exists_and_invalid_source_return_expected_flags() {
+ run_case(
+ "z-stdlib-json-exists-and-invalid",
+ r#"
+get fun `test z stdlib json exists and invalid source`() {
+ val src = fs.readFile("fixtures/metadata.json")!;
+
+ expect(json.exists(src, "/token")).toEqual(true);
+ expect(json.exists(src, "/frozen")).toEqual(true);
+ expect(json.exists(src, "/missing")).toEqual(false);
+
+ // Invalid JSON collapses to null / false rather than throwing.
+ expect(json.getString("this is not json", "/x")).toBeNull();
+ expect(json.exists("this is not json", "/x")).toEqual(false);
+}
+"#,
+ );
+}
diff --git a/tests/integration/test_runner/mod.rs b/tests/integration/test_runner/mod.rs
index 2d01f585a5..94b0299bd7 100644
--- a/tests/integration/test_runner/mod.rs
+++ b/tests/integration/test_runner/mod.rs
@@ -86,6 +86,7 @@ mod get_account_state_after_top_up_returns_account_info_with_expected_balance_te
mod get_deployed_code_transitions_from_null_to_non_null_in_project_builder_tests;
mod get_method_current_balance_matches_account_balance_tests;
mod global_version_roundtrip_persists_after_net_set_config_tests;
+mod json_reads_typed_values_by_pointer_tests;
mod keeps_stdout_and_stderr_separated_when_stderr_happens_first_tests;
mod load_library_prefers_world_state_library_before_network_tests;
mod load_library_unknown_hash_returns_null_in_project_builder_tests;