crates: add JsonObjectOrArray and JsonScalar types for raw json rpc parameters
What changed, and why it matters
This commit adds new Rust types and matching gRPC protobuf messages for carrying arbitrary JSON data (objects, arrays, and scalar values) through Core Lightning's RPC and plugin interfaces. It is a pure infrastructure/serialization change with no security-relevant behavior visible in the diff.
No security action required for this commit. Treat as routine code review for a new data-type addition. Monitor the follow-up commits that wire these types into the `rpc_command` hook for any security-relevant behavior.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces JsonObjectOrArray and JsonScalar enums in cln-rpc/src/primitives.rs and corresponding protobuf messages in cln-grpc/proto/primitives.proto, plus bidirectional From conversions between serde_json::Value and the generated gRPC types in cln-grpc/src/pb.rs. It also moves serde_json from dev-dependencies to normal dependencies in cln-grpc/Cargo.toml. The commit message states these types will be used later for the rpc_command hook. No parsing logic, authorization checks, cryptographic operations, or network handling is changed.
Changed components
cln-rpc/src/primitives.rscln-grpc/proto/primitives.protocln-grpc/src/pb.rscln-grpc/Cargo.tomlInspect captured patch +198 / −2
diff --git a/cln-grpc/Cargo.toml b/cln-grpc/Cargo.toml
index b221e47e..c5a2b04c 100644
--- a/cln-grpc/Cargo.toml
+++ b/cln-grpc/Cargo.toml
@@ -28,7 +28,6 @@ tokio = { version = "1.36.0", features = ["sync"] }
futures-core = "0.3.30"
tokio-util = "0.7.10"
-[dev-dependencies]
serde_json = "1.0.72"
[build-dependencies]
diff --git a/cln-grpc/proto/primitives.proto b/cln-grpc/proto/primitives.proto
index fa62fdcf..b91d5292 100644
--- a/cln-grpc/proto/primitives.proto
+++ b/cln-grpc/proto/primitives.proto
@@ -147,3 +147,40 @@ enum PluginSubcommand {
STARTDIR = 3;
LIST = 4;
}
+
+message JsonObjectOrArray {
+ oneof structure {
+ JsonObject object = 1;
+ JsonArray array = 2;
+ }
+}
+
+message JsonObject {
+ map<string, JsonValue> fields = 1;
+}
+
+message JsonArray {
+ repeated JsonValue values = 1;
+}
+
+message JsonValue {
+ oneof kind {
+ bool bool_value = 1;
+ int64 int_value = 2;
+ uint64 uint_value = 3;
+ double double_value = 4;
+ string string_value = 5;
+ JsonArray array = 6;
+ JsonObject object = 7;
+ }
+}
+
+message JsonScalar {
+ oneof scalar {
+ bool bool_value = 1;
+ int64 int_value = 2;
+ uint64 uint_value = 3;
+ double double_value = 4;
+ string string_value = 5;
+ }
+}
diff --git a/cln-grpc/src/pb.rs b/cln-grpc/src/pb.rs
index b518703b..a1935cf6 100644
--- a/cln-grpc/src/pb.rs
+++ b/cln-grpc/src/pb.rs
@@ -10,7 +10,8 @@ mod convert {
use cln_rpc::primitives::{
Amount as JAmount, AmountOrAll as JAmountOrAll, AmountOrAny as JAmountOrAny,
- Feerate as JFeerate, Outpoint as JOutpoint, OutputDesc as JOutputDesc,
+ Feerate as JFeerate, JsonObjectOrArray as JJsonObjectOrArray, JsonScalar as JJsonScalar,
+ Outpoint as JOutpoint, OutputDesc as JOutputDesc,
};
impl From<JAmount> for Amount {
@@ -281,6 +282,149 @@ mod convert {
}
}
+ impl From<serde_json::Value> for JsonValue {
+ fn from(v: serde_json::Value) -> Self {
+ let kind = match v {
+ serde_json::Value::Null => None,
+ serde_json::Value::Bool(b) => Some(json_value::Kind::BoolValue(b)),
+ serde_json::Value::Number(n) => {
+ if let Some(u) = n.as_u64() {
+ Some(json_value::Kind::UintValue(u))
+ } else if let Some(i) = n.as_i64() {
+ Some(json_value::Kind::IntValue(i))
+ } else if let Some(f) = n.as_f64() {
+ Some(json_value::Kind::DoubleValue(f))
+ } else {
+ let error = format!("Failed to parse number: `{}`", n);
+ println!(
+ "{}",
+ serde_json::json!({"jsonrpc": "2.0",
+ "method": "log",
+ "params": {"level":"warn", "message": error}})
+ );
+ std::process::exit(1);
+ }
+ }
+ serde_json::Value::String(s) => Some(json_value::Kind::StringValue(s)),
+ serde_json::Value::Array(arr) => Some(json_value::Kind::Array(JsonArray {
+ values: arr.into_iter().map(JsonValue::from).collect(),
+ })),
+ serde_json::Value::Object(obj) => Some(json_value::Kind::Object(JsonObject {
+ fields: obj
+ .into_iter()
+ .map(|(k, v)| (k, JsonValue::from(v)))
+ .collect(),
+ })),
+ };
+ JsonValue { kind }
+ }
+ }
+
+ impl From<JJsonObjectOrArray> for JsonObjectOrArray {
+ fn from(v: JJsonObjectOrArray) -> Self {
+ let structure = match v {
+ JJsonObjectOrArray::Array(arr) => {
+ Some(json_object_or_array::Structure::Array(JsonArray {
+ values: arr.into_iter().map(JsonValue::from).collect(),
+ }))
+ }
+ JJsonObjectOrArray::Object(obj) => {
+ Some(json_object_or_array::Structure::Object(JsonObject {
+ fields: obj
+ .into_iter()
+ .map(|(k, v)| (k, JsonValue::from(v)))
+ .collect(),
+ }))
+ }
+ };
+ JsonObjectOrArray { structure }
+ }
+ }
+
+ impl From<JsonValue> for serde_json::Value {
+ fn from(v: JsonValue) -> Self {
+ match v.kind {
+ None => serde_json::Value::Null,
+ Some(json_value::Kind::BoolValue(b)) => serde_json::Value::Bool(b),
+ Some(json_value::Kind::UintValue(u)) => serde_json::Value::Number(u.into()),
+ Some(json_value::Kind::IntValue(i)) => serde_json::Value::Number(i.into()),
+ Some(json_value::Kind::DoubleValue(f)) => match serde_json::Number::from_f64(f) {
+ Some(num) => serde_json::Value::Number(num),
+ None => {
+ let error = format!("Failed to parse number: `{}`", f);
+ println!(
+ "{}",
+ serde_json::json!({"jsonrpc": "2.0",
+ "method": "log",
+ "params": {"level":"warn", "message": error}})
+ );
+ std::process::exit(1);
+ }
+ },
+ Some(json_value::Kind::StringValue(s)) => serde_json::Value::String(s),
+ Some(json_value::Kind::Array(arr)) => serde_json::Value::Array(
+ arr.values
+ .into_iter()
+ .map(serde_json::Value::from)
+ .collect(),
+ ),
+ Some(json_value::Kind::Object(obj)) => serde_json::Value::Object(
+ obj.fields
+ .into_iter()
+ .map(|(k, v)| (k, serde_json::Value::from(v)))
+ .collect(),
+ ),
+ }
+ }
+ }
+
+ impl From<JsonObjectOrArray> for JJsonObjectOrArray {
+ fn from(v: JsonObjectOrArray) -> Self {
+ match v.structure {
+ Some(json_object_or_array::Structure::Array(arr)) => JJsonObjectOrArray::Array(
+ arr.values
+ .into_iter()
+ .map(serde_json::Value::from)
+ .collect(),
+ ),
+ Some(json_object_or_array::Structure::Object(obj)) => JJsonObjectOrArray::Object(
+ obj.fields
+ .into_iter()
+ .map(|(k, v)| (k, serde_json::Value::from(v)))
+ .collect(),
+ ),
+ None => JJsonObjectOrArray::Array(vec![]), // or handle as error
+ }
+ }
+ }
+
+ impl From<JsonScalar> for JJsonScalar {
+ fn from(v: JsonScalar) -> Self {
+ match v.scalar {
+ None => JJsonScalar::Null,
+ Some(json_scalar::Scalar::BoolValue(b)) => JJsonScalar::Bool(b),
+ Some(json_scalar::Scalar::IntValue(i)) => JJsonScalar::Number(i.into()),
+ Some(json_scalar::Scalar::DoubleValue(d)) => {
+ match serde_json::Number::from_f64(d) {
+ Some(num) => JJsonScalar::Number(num),
+ None => {
+ let error = format!("Failed to parse number: `{}`", d);
+ println!(
+ "{}",
+ serde_json::json!({"jsonrpc": "2.0",
+ "method": "log",
+ "params": {"level":"warn", "message": error}})
+ );
+ std::process::exit(1);
+ }
+ }
+ }
+ Some(json_scalar::Scalar::UintValue(u)) => JJsonScalar::Number(u.into()),
+ Some(json_scalar::Scalar::StringValue(s)) => JJsonScalar::String(s),
+ }
+ }
+ }
+
#[cfg(test)]
mod test {
use super::*;
diff --git a/cln-rpc/src/primitives.rs b/cln-rpc/src/primitives.rs
index 25f56103..8ee27f62 100644
--- a/cln-rpc/src/primitives.rs
+++ b/cln-rpc/src/primitives.rs
@@ -1173,3 +1173,19 @@ impl Serialize for TlvStream {
map.end()
}
}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(untagged)]
+pub enum JsonObjectOrArray {
+ Object(serde_json::Map<String, Value>),
+ Array(Vec<Value>),
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(untagged)]
+pub enum JsonScalar {
+ String(String),
+ Number(serde_json::Number),
+ Bool(bool),
+ Null,
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.