From c020c21d543a14268b98995d1a9d1878f3d95ec2 Mon Sep 17 00:00:00 2001 From: sedited Date: Mon, 10 Aug 2026 16:00:47 +0200 Subject: [PATCH 1/3] rpc: Handle skip type args for openrpc Instead of filling them in with empty object and array args be a bit more friendly to the consumer by giving type hints, while retaining type flexibility: Keep the empty object, but fill the array with the hinted at types by recursing through them. Add an additional argument to the openrpc functions (`in_skip_type_check`) to keep track of when a loosely typed argument is under evaluation. Note that the array is evaluated stricter than before: If it contains items, they need to match the nested objects. The change from oneOf->anyOf and removing additionalProperties: false should still convey to the reader that this is a loosely typed object and prevent collisions between these loosely-typed schemas. This was initially motivated by testing the dump of the schema against open-rpc-generator, which crashed with: ``` open-rpc-generator generate -t client -l rust -n bitcoin_client -d ./openrpc.gen.json -o ./generated There was error at generator runtime: TypeError: Cannot convert undefined or null to object ``` The changes here fix this crash, but I think creating a friendlier output is helpful on its own. This patch changes the schema exported from CreateTxDoc as follows: ```diff diff -U6 <(jq '.methods[] | select(.name == "createrawtransaction")' dump.json) \ <(jq '.methods[] | select(.name == "createrawtransaction")' new_dump.json) @@ -37,13 +37,43 @@ { "name": "outputs", "required": true, "schema": { "oneOf": [ { - "type": "array" + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + }, + { + "type": "object", + "properties": { + "data": { + "type": "string", + "pattern": "^[0-9a-fA-F]+$", + "description": "A key-value pair. The key must be \"data\", the value is hex-encoded data that becomes a part of an OP_RETURN output" + } + }, + "required": [ + "data" + ] + } + ] + } }, { "type": "object" } ] }, ``` --- src/rpc/server.cpp | 26 +++++++++++++++----------- test/functional/rpc_openrpc.py | 5 ++++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index 4b37cc8c42c..73b0d248f6f 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -243,7 +243,7 @@ static RPCMethod getrpcinfo() } namespace { -UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden); +UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden, bool in_skip_type_check); UniValue OpenRPCResultSchema(const RPCResult& result); UniValue MakeObject(std::initializer_list> entries) @@ -262,21 +262,21 @@ void PushUniqueSchema(UniValue& schemas, std::unordered_set& seen, } // NOLINTNEXTLINE(misc-no-recursion) -UniValue DedupArrayItemsSchema(std::span inner, bool include_hidden) +UniValue DedupArrayItemsSchema(std::span inner, bool include_hidden, bool in_skip_type_check) { if (inner.empty()) return UniValue{UniValue::VOBJ}; - if (inner.size() == 1) return OpenRPCArgSchema(inner.front(), include_hidden); + if (inner.size() == 1) return OpenRPCArgSchema(inner.front(), include_hidden, in_skip_type_check); UniValue one_of{UniValue::VARR}; std::unordered_set seen; for (const auto& item : inner) { - PushUniqueSchema(one_of, seen, OpenRPCArgSchema(item, include_hidden)); + PushUniqueSchema(one_of, seen, OpenRPCArgSchema(item, include_hidden, in_skip_type_check)); } if (one_of.size() == 1) return one_of[0]; UniValue items{UniValue::VOBJ}; - items.pushKV("oneOf", std::move(one_of)); + items.pushKV(in_skip_type_check ? "anyOf" : "oneOf", std::move(one_of)); return items; } @@ -331,14 +331,18 @@ void ApplyArgFallback(UniValue& schema, const RPCArg& arg) } // NOLINTNEXTLINE(misc-no-recursion) -UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden) +UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden, bool in_skip_type_check) { UniValue schema{UniValue::VOBJ}; if (arg.m_opts.skip_type_check) { ApplyTypeStrOverride(schema, arg); if (schema.empty() && arg.m_type == RPCArg::Type::ARR) { + UniValue items{UniValue::VOBJ}; + items.pushKV("type", "array"); + items.pushKV("items", DedupArrayItemsSchema(arg.m_inner, include_hidden, /*in_skip_type_check=*/true)); + UniValue one_of{UniValue::VARR}; - one_of.push_back(MakeObject({{"type", "array"}})); + one_of.push_back(std::move(items)); one_of.push_back(MakeObject({{"type", "object"}})); schema.pushKV("oneOf", std::move(one_of)); } @@ -383,7 +387,7 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden) break; } case RPCArg::Type::ARR: { - UniValue items{DedupArrayItemsSchema(arg.m_inner, include_hidden)}; + UniValue items{DedupArrayItemsSchema(arg.m_inner, include_hidden, in_skip_type_check)}; schema.pushKV("type", "array"); schema.pushKV("items", std::move(items)); break; @@ -394,7 +398,7 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden) UniValue required{UniValue::VARR}; for (const auto& inner : arg.m_inner) { if (!include_hidden && inner.m_opts.hidden) continue; - UniValue prop{OpenRPCArgSchema(inner, include_hidden)}; + UniValue prop{OpenRPCArgSchema(inner, include_hidden, in_skip_type_check)}; if (!inner.m_description.empty()) prop.pushKV("description", inner.m_description); if (inner.m_opts.placeholder) prop.pushKV("x-bitcoin-placeholder", true); if (inner.m_opts.also_positional) prop.pushKV("x-bitcoin-also-positional", true); @@ -410,7 +414,7 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden) case RPCArg::Type::OBJ_USER_KEYS: { schema.pushKV("type", "object"); if (!arg.m_inner.empty()) { - schema.pushKV("additionalProperties", OpenRPCArgSchema(arg.m_inner[0], include_hidden)); + schema.pushKV("additionalProperties", OpenRPCArgSchema(arg.m_inner[0], include_hidden, in_skip_type_check)); } else { schema.pushKV("additionalProperties", true); } @@ -911,7 +915,7 @@ UniValue CRPCTable::buildOpenRPCDoc(bool include_hidden) const UniValue param{UniValue::VOBJ}; param.pushKV("name", arg.GetFirstName()); param.pushKV("required", !arg.IsOptional()); - param.pushKV("schema", OpenRPCArgSchema(arg, include_hidden)); + param.pushKV("schema", OpenRPCArgSchema(arg, include_hidden, /*in_skip_type_check=*/false)); std::vector names{SplitString(arg.m_names, '|')}; if (names.size() > 1) { diff --git a/test/functional/rpc_openrpc.py b/test/functional/rpc_openrpc.py index 91195b6d30a..d25d6daff0b 100755 --- a/test/functional/rpc_openrpc.py +++ b/test/functional/rpc_openrpc.py @@ -75,7 +75,10 @@ class OpenRPCDocTest(BitcoinTestFramework): self.log.info("Checking relaxed schemas for unchecked RPC types") createrawtransaction = find_method(openrpc, "createrawtransaction") outputs = find_param(createrawtransaction, "outputs") - assert_equal(outputs["schema"], {"oneOf": [{"type": "array"}, {"type": "object"}]}) + address_obj = { "type": "object", "additionalProperties": {"oneOf": [{"type": "number"},{"type": "string"}]}} + data_description = "A key-value pair. The key must be \"data\", the value is hex-encoded data that becomes a part of an OP_RETURN output" + data_obj = {"type": "object", "properties": { "data": {"type": "string", "pattern": "^[0-9a-fA-F]+$", "description": data_description}}, "additionalProperties": False, "required": ["data"]} + assert_equal(outputs["schema"], {"oneOf": [{"type": "array", "items": {"anyOf": [address_obj, data_obj]}}, {"type": "object"}]}) getdescriptoractivity = find_method(openrpc, "getdescriptoractivity") activity = getdescriptoractivity["result"]["schema"]["properties"]["activity"] From c94074fa1b1396e310ab94955f5d04c9bda61b64 Mon Sep 17 00:00:00 2001 From: sedited Date: Mon, 10 Aug 2026 16:05:12 +0200 Subject: [PATCH 2/3] rpc: Surface OBJ_USER_KEYS description for openrpc While this is usually used where the types are not enforced strictly, adding the description is both useful to the developer implementing a client and for potentially using the openrpc output as a basis for documentation. ```diff diff interim_dump.json new_dump.json 465c465,466 < } --- > }, > "description": "A key-value pair. The key (string) is the bitcoin address,\nthe value (float or string) is the amount in BTC" 777c778,779 < } --- > }, > "description": "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in BTC" 903c905,906 < } --- > }, > "description": "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in BTC" 12915c12918,12919 < } --- > }, > "description": "A key-value pair. The key (string) is the bitcoin address,\nthe value (float or string) is the amount in BTC" 13692c13696,13697 < } --- > }, > "description": "A key-value pair. The key (string) is the bitcoin address,\nthe value (float or string) is the amount in BTC" 14002c14007,14008 < } --- > }, > "description": "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in BTC" 14254c14260,14261 < } --- > }, > "description": "The bitcoin address is the key, the numeric amount (can be string) in BTC is the value" 16046c16053,16054 < } --- > }, > "description": "A key-value pair. The key (string) is the bitcoin address,\nthe value (float or string) is the amount in BTC" ``` --- src/rpc/server.cpp | 3 +++ test/functional/rpc_openrpc.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index 73b0d248f6f..5f3bf91683b 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -415,6 +415,9 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden, bool in_skip_t schema.pushKV("type", "object"); if (!arg.m_inner.empty()) { schema.pushKV("additionalProperties", OpenRPCArgSchema(arg.m_inner[0], include_hidden, in_skip_type_check)); + if (!arg.m_inner[0].m_description.empty()) { + schema.pushKV("description", arg.m_inner[0].m_description); + } } else { schema.pushKV("additionalProperties", true); } diff --git a/test/functional/rpc_openrpc.py b/test/functional/rpc_openrpc.py index d25d6daff0b..5a57e6251ad 100755 --- a/test/functional/rpc_openrpc.py +++ b/test/functional/rpc_openrpc.py @@ -75,7 +75,8 @@ class OpenRPCDocTest(BitcoinTestFramework): self.log.info("Checking relaxed schemas for unchecked RPC types") createrawtransaction = find_method(openrpc, "createrawtransaction") outputs = find_param(createrawtransaction, "outputs") - address_obj = { "type": "object", "additionalProperties": {"oneOf": [{"type": "number"},{"type": "string"}]}} + address_description = "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in BTC" + address_obj = {"type": "object", "additionalProperties": {"oneOf": [{"type": "number"},{"type": "string"}]}, "description": address_description} data_description = "A key-value pair. The key must be \"data\", the value is hex-encoded data that becomes a part of an OP_RETURN output" data_obj = {"type": "object", "properties": { "data": {"type": "string", "pattern": "^[0-9a-fA-F]+$", "description": data_description}}, "additionalProperties": False, "required": ["data"]} assert_equal(outputs["schema"], {"oneOf": [{"type": "array", "items": {"anyOf": [address_obj, data_obj]}}, {"type": "object"}]}) From e07d826e0ebd9507793fe033236e5f0f12ba5732 Mon Sep 17 00:00:00 2001 From: Shuvam Pandey Date: Fri, 14 Aug 2026 16:25:40 +0200 Subject: [PATCH 3/3] rpc: Fix type in ApplyTypeStrOverride This should be an integer, not a numeric, as already enforced by the RPC code and described in the mapping just above the changed line. --- src/rpc/server.cpp | 2 +- test/functional/rpc_openrpc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index 5f3bf91683b..1bc8933e624 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -311,7 +311,7 @@ void ApplyTypeStrOverride(UniValue& schema, const RPCArg& arg) }; if (number_or_string.contains(type_label)) { UniValue one_of{UniValue::VARR}; - one_of.push_back(MakeObject({{"type", "number"}})); + one_of.push_back(MakeObject({{"type", "integer"}})); one_of.push_back(MakeObject({{"type", "string"}})); schema = UniValue{UniValue::VOBJ}; schema.pushKV("oneOf", std::move(one_of)); diff --git a/test/functional/rpc_openrpc.py b/test/functional/rpc_openrpc.py index 5a57e6251ad..32be191f58f 100755 --- a/test/functional/rpc_openrpc.py +++ b/test/functional/rpc_openrpc.py @@ -52,7 +52,7 @@ class OpenRPCDocTest(BitcoinTestFramework): self.log.info("Checking type_str override schemas") getblockstats = find_method(openrpc, "getblockstats") hash_or_height = find_param(getblockstats, "hash_or_height") - assert_equal(hash_or_height["schema"], {"oneOf": [{"type": "number"}, {"type": "string"}]}) + assert_equal(hash_or_height["schema"], {"oneOf": [{"type": "integer"}, {"type": "string"}]}) self.log.info("Checking fixed-length array schemas") deriveaddresses = find_method(openrpc, "deriveaddresses")