JSON Reference
JSON Reference
- Overview
- Defining JSON Columns
- Creating JSON Values
- JSON Filtering
- Filter Operations
- Combining JSON Filters
- Type Conversion
- Complete Example
- Error Handling
Overview
The Json data type allows you to store and query semi-structured JSON data within your database tables. This is useful for:
- Flexible schemas where structure varies between records
- Metadata storage
- User preferences and settings
- Any scenario where data structure may evolve
Defining JSON Columns
To use JSON in your schema, use the Json type:
use ic_dbms_api::prelude::*;
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "users"]
pub struct User {
#[primary_key]
pub id: Uint32,
pub name: Text,
pub metadata: Json, // Required JSON field
pub settings: Nullable<Json>, // Optional JSON field
}
Creating JSON Values
From string:
use std::str::FromStr;
use ic_dbms_api::prelude::Json;
let json = Json::from_str(r#"{"name": "Alice", "age": 30}"#).unwrap();
From serde_json::Value:
use serde_json::json;
use ic_dbms_api::prelude::Json;
let json: Json = json!({
"name": "Alice",
"age": 30,
"tags": ["developer", "rust"],
"address": {
"city": "New York",
"country": "US"
}
}).into();
In insert requests:
let user = UserInsertRequest {
id: 1.into(),
name: "Alice".into(),
metadata: json!({
"role": "admin",
"permissions": ["read", "write", "delete"]
}).into(),
settings: Nullable::Value(json!({
"theme": "dark",
"notifications": true
}).into()),
};
JSON Filtering
ic-dbms provides powerful JSON filtering through the JsonFilter enum:
- Contains: Check if JSON contains a pattern (structural containment)
- Extract: Extract value at path and compare
- HasKey: Check if a path exists
Path Syntax
Paths use dot notation with bracket array indices:
| Path | Meaning |
|---|---|
"name" |
Root-level field name |
"user.name" |
Nested field at user.name |
"items[0]" |
First element of items array |
"users[0].name" |
name field of first user |
"data[0][1]" |
Nested array access |
"[0]" |
First element of root array |
Path examples:
{
"name": "Alice", // Path: "name"
"user": {
"email": "a@b.com" // Path: "user.email"
},
"tags": ["a", "b", "c"], // Path: "tags[0]" = "a"
"matrix": [[1,2], [3,4]] // Path: "matrix[1][0]" = 3
}
Filter Operations
Contains (Structural Containment)
Checks if the JSON column contains a specified pattern. Implements PostgreSQL @> style containment:
- Objects: All key-value pairs in pattern must exist in target (recursive)
- Arrays: All elements in pattern must exist in target (order-independent)
- Primitives: Must be equal
use ic_dbms_api::prelude::*;
use std::str::FromStr;
// Filter where metadata contains {"active": true}
let pattern = Json::from_str(r#"{"active": true}"#).unwrap();
let filter = Filter::json("metadata", JsonFilter::contains(pattern));
Containment behavior:
| Target | Pattern | Result |
|---|---|---|
{"a": 1, "b": 2} |
{"a": 1} |
Match |
{"a": 1} |
{"a": 1, "b": 2} |
No match |
{"user": {"name": "Alice", "age": 30}} |
{"user": {"name": "Alice"}} |
Match |
[1, 2, 3] |
[3, 1] |
Match (order-independent) |
[1, 2] |
[1, 2, 3] |
No match |
{"tags": ["a", "b", "c"]} |
{"tags": ["b"]} |
Match |
Use cases:
- Check if user has specific role:
contains({"role": "admin"}) - Check if array contains value:
contains({"tags": ["important"]}) - Check nested properties:
contains({"settings": {"theme": "dark"}})
Extract (Path Extraction + Comparison)
Extract a value at path and apply comparison:
// Equal
let filter = Filter::json("metadata",
JsonFilter::extract_eq("user.name", Value::Text("Alice".into()))
);
// Greater than
let filter = Filter::json("metadata",
JsonFilter::extract_gt("user.age", Value::Int64(18.into()))
);
// In list
let filter = Filter::json("metadata",
JsonFilter::extract_in("status", vec![
Value::Text("active".into()),
Value::Text("pending".into()),
])
);
// Is null (path doesn't exist or value is null)
let filter = Filter::json("metadata",
JsonFilter::extract_is_null("deleted_at")
);
// Not null (path exists and value is not null)
let filter = Filter::json("metadata",
JsonFilter::extract_not_null("email")
);
Available comparison methods:
| Method | Description |
|---|---|
extract_eq(path, value) |
Equal |
extract_ne(path, value) |
Not equal |
extract_gt(path, value) |
Greater than |
extract_lt(path, value) |
Less than |
extract_ge(path, value) |
Greater than or equal |
extract_le(path, value) |
Less than or equal |
extract_in(path, values) |
Value in list |
extract_is_null(path) |
Path doesn’t exist or is null |
extract_not_null(path) |
Path exists and is not null |
HasKey (Path Existence)
Check if a path exists in the JSON:
// Check for root-level key
let filter = Filter::json("metadata", JsonFilter::has_key("email"));
// Check for nested path
let filter = Filter::json("metadata", JsonFilter::has_key("user.address.city"));
// Check for array element
let filter = Filter::json("metadata", JsonFilter::has_key("items[0]"));
Note:
HasKeyreturnstrueeven if the value at path isnull. It only checks for path existence.
Combining JSON Filters
JSON filters combine with other filters using and(), or(), not():
// has email AND age > 18
let filter = Filter::json("metadata", JsonFilter::has_key("email"))
.and(Filter::json("metadata", JsonFilter::extract_gt("age", Value::Int64(18.into()))));
// role = "admin" OR role = "moderator"
let filter = Filter::json("metadata", JsonFilter::extract_eq("role", Value::Text("admin".into())))
.or(Filter::json("metadata", JsonFilter::extract_eq("role", Value::Text("moderator".into()))));
// Combine with regular filters
let pattern = Json::from_str(r#"{"active": true}"#).unwrap();
let filter = Filter::eq("id", Value::Int32(1.into()))
.and(Filter::json("metadata", JsonFilter::contains(pattern)));
// NOT has deleted_at
let filter = Filter::json("metadata", JsonFilter::has_key("deleted_at")).not();
Type Conversion
When extracting JSON values, they’re converted to DBMS types:
| JSON Type | DBMS Value |
|---|---|
null |
Value::Null |
true/false |
Value::Boolean |
| Integer number | Value::Int64 |
| Float number | Value::Decimal |
| String | Value::Text |
| Array | Value::Json |
| Object | Value::Json |
Comparison examples:
// JSON: {"count": 42}
// Extracted as Int64, compare with Int64
JsonFilter::extract_eq("count", Value::Int64(42.into()))
// JSON: {"price": 19.99}
// Extracted as Decimal, compare with Decimal
JsonFilter::extract_gt("price", Value::Decimal(10.0.into()))
// JSON: {"active": true}
// Extracted as Boolean
JsonFilter::extract_eq("active", Value::Boolean(true))
// JSON: {"name": "Alice"}
// Extracted as Text
JsonFilter::extract_eq("name", Value::Text("Alice".into()))
Complete Example
use ic_dbms_api::prelude::*;
use std::str::FromStr;
#[derive(Debug, Table, CandidType, Deserialize, Clone, PartialEq, Eq)]
#[table = "products"]
pub struct Product {
#[primary_key]
pub id: Uint32,
pub name: Text,
pub attributes: Json, // {"color": "red", "size": "M", "tags": ["sale", "new"], "price": 29.99}
}
async fn example_queries(client: &impl Client) -> Result<(), Box<dyn std::error::Error>> {
// Find all red products
let filter = Filter::json("attributes",
JsonFilter::extract_eq("color", Value::Text("red".into()))
);
let query = Query::builder().filter(filter).build();
let red_products = client.select::<Product>(Product::table_name(), query, None).await??;
// Find products with "sale" tag
let pattern = Json::from_str(r#"{"tags": ["sale"]}"#)?;
let filter = Filter::json("attributes", JsonFilter::contains(pattern));
let query = Query::builder().filter(filter).build();
let sale_products = client.select::<Product>(Product::table_name(), query, None).await??;
// Find products with size attribute
let filter = Filter::json("attributes", JsonFilter::has_key("size"));
let query = Query::builder().filter(filter).build();
let sized_products = client.select::<Product>(Product::table_name(), query, None).await??;
// Find red products with price > 20
let filter = Filter::json("attributes", JsonFilter::extract_eq("color", Value::Text("red".into())))
.and(Filter::json("attributes", JsonFilter::extract_gt("price", Value::Decimal(20.0.into()))));
let query = Query::builder().filter(filter).build();
let expensive_red = client.select::<Product>(Product::table_name(), query, None).await??;
// Find products in specific sizes
let filter = Filter::json("attributes",
JsonFilter::extract_in("size", vec![
Value::Text("S".into()),
Value::Text("M".into()),
Value::Text("L".into()),
])
);
let query = Query::builder().filter(filter).build();
let standard_sizes = client.select::<Product>(Product::table_name(), query, None).await??;
Ok(())
}
Error Handling
JSON filter operations return errors for:
Invalid path syntax:
- Empty paths
- Trailing dots (
"user.") - Unclosed brackets (
"items[0") - Negative indices (
"items[-1]") - Non-numeric array indices (
"items[abc]")
Non-JSON column:
- Applying JSON filter to a non-JSON column
// Invalid path - will error
let filter = Filter::json("metadata", JsonFilter::has_key("user.")); // Trailing dot
// Non-JSON column - will error
let filter = Filter::json("name", JsonFilter::has_key("field")); // "name" is Text, not Json
let result = client.select::<User>(User::table_name(),
Query::builder().filter(filter).build(),
None
).await?;
match result {
Err(IcDbmsError::Query(QueryError::InvalidQuery)) => {
println!("Invalid JSON filter");
}
_ => {}
}