RFC 012: JsonValue Type for Dynamic JSON¶
Status: Draft
Summary¶
Add a JsonValue type for handling JSON with unknown or varying structure at runtime.
Motivation¶
Currently, Incan requires defining models with @derive(Serialize, Deserialize) for JSON handling.
This works well for known, fixed schemas but falls short for:
- Dynamic APIs — APIs that return varying structures depending on context
- Exploration — Prototyping without defining full models
- Partial parsing — Extracting specific fields from large JSON without modeling everything
- Mixed schemas — JSON where some parts are typed and others are dynamic
Current Approach (Works)¶
@derive(Serialize, Deserialize)
model User:
name: str
age: int
user = User.from_json(json_str)?
println(user.name)
Proposed Addition¶
# Parse unknown JSON
data = JsonValue.parse(json_str)?
# Access dynamically
name = data["user"]["name"].as_str()
count = data["count"].as_int()
items = data["items"].as_list()
# Check types at runtime
if data["field"].is_string():
println(data["field"].as_str())
Detailed Design¶
JsonValue Type¶
JsonValue is an enum representing any JSON value:
enum JsonValue:
Null
Bool(bool)
Int(int)
Float(float)
String(str)
Array(List[JsonValue])
Object(Dict[str, JsonValue])
Constructors¶
# Parse from string
value = JsonValue.parse(json_str) -> Result[JsonValue, str]
# Create values directly
null_val = JsonValue.null()
bool_val = JsonValue.bool(true)
int_val = JsonValue.int(42)
str_val = JsonValue.string("hello")
arr_val = JsonValue.array([val1, val2])
obj_val = JsonValue.object({"key": value})
Access Methods¶
# Type checking
value.is_null() -> bool
value.is_bool() -> bool
value.is_int() -> bool
value.is_float() -> bool
value.is_string() -> bool
value.is_array() -> bool
value.is_object() -> bool
# Value extraction (returns Option)
value.as_bool() -> Option[bool]
value.as_int() -> Option[int]
value.as_float() -> Option[float]
value.as_str() -> Option[str]
value.as_array() -> Option[List[JsonValue]]
value.as_object() -> Option[Dict[str, JsonValue]]
Indexing¶
# Object field access
value["key"] -> JsonValue # Returns JsonValue.Null if missing
# Array index access
value[0] -> JsonValue # Returns JsonValue.Null if out of bounds
# Chained access
value["user"]["address"]["city"].as_str()
Serialization¶
value.to_json() -> str # Serialize back to JSON string
Rust Implementation¶
Maps to serde_json::Value:
pub type JsonValue = serde_json::Value;
impl JsonValue {
pub fn parse(s: &str) -> Result<Self, String> {
serde_json::from_str(s).map_err(|e| e.to_string())
}
pub fn is_null(&self) -> bool { self.is_null() }
pub fn as_str(&self) -> Option<&str> { self.as_str() }
// ... etc
}
Hybrid Models¶
Mix typed and dynamic:
@derive(Serialize, Deserialize)
model ApiResponse:
status: int
message: str
data: JsonValue # Dynamic payload
Alternatives Considered¶
1. Dict[str, Any]¶
Rust doesn't have Any like Python. Would require boxing and type erasure.
2. Generic parsing only¶
Keep json_parse[T]() and require models for everything. Rejected because:
- Too restrictive for dynamic use cases
- Poor developer experience for exploration
3. Automatic Dict conversion¶
Auto-convert JSON objects to Dict[str, ???]. Rejected because:
- Loses type information
- Can't handle nested structures uniformly
Implementation Plan¶
- Add
JsonValueas a language surface type inincan_core::lang::surface(maps toserde_json::Value) - Implement constructors and methods in codegen
- Add typechecker support for indexing operations
- Add
JsonValuefield support in models - Document with examples
Open Questions¶
- Should
value["key"]returnJsonValueorOption[JsonValue]? - Returning
JsonValue(with Null for missing) is more ergonomic for chaining -
Returning
Optionis more explicit about missing keys -
Should we support
value.keysyntax as sugar forvalue["key"]? -
How to handle numeric types? JSON has only one number type, but Incan has
intandfloat.
References¶
- Rust:
serde_json::Value - Python:
dictfromjson.loads() - TypeScript:
anyorunknown - Go:
interface{}/any