nim-serde JSON
Explore JSON serialization and deserialization using nim-serde, an improved alternative to std/json.
Table of Contents
- nim-serde JSON
Serialization API
The nim-serde JSON serialization API provides several ways to convert Nim values to JSON.
Basic Serialization with % operator
The % operator converts Nim values to JsonNode objects, which can then be converted to JSON strings:
import pkg/serde/json
# Basic types
assert %42 == newJInt(42)
assert %"hello" == newJString("hello")
assert %true == newJBool(true)
# Arrays and sequences
let arr = newJArray()
arr.add(newJInt(1))
arr.add(newJInt(2))
arr.add(newJInt(3))
assert $(%[1, 2, 3]) == $arr
Object Serialization
Objects can be serialized using the % operator, which automatically handles field serialization based on the object's configuration:
import pkg/serde/json
type Person = object
name {.serialize.}: string
age {.serialize.}: int
address: string # Not serialized by default in OptIn mode
let person = Person(name: "John", age: 30, address: "123 Main St")
let jsonNode = %person
assert jsonNode.kind == JObject
assert jsonNode.len == 2
assert jsonNode["name"].getStr == "John"
assert jsonNode["age"].getInt == 30
assert "address" notin jsonNode
Serialization with %*
The %* macro provides a more convenient way to create JSON objects:
import pkg/serde/json
let jsonObj = %*{
"name": "John",
"age": 30,
"hobbies": ["reading", "coding"],
"address": {
"street": "123 Main St",
"city": "Anytown"
}
}
assert jsonObj.kind == JObject
assert jsonObj["name"].getStr == "John"
assert jsonObj["age"].getInt == 30
assert jsonObj["hobbies"].kind == JArray
assert jsonObj["hobbies"][0].getStr == "reading"
assert jsonObj["address"]["street"].getStr == "123 Main St"
Converting to JSON String with toJson
The toJson function converts any serializable value directly to a JSON string:
import pkg/serde/json
type Person = object
name {.serialize.}: string
age {.serialize.}: int
let person = Person(name: "John", age: 30)
assert person.toJson == """{"name":"John","age":30}"""
Serialization Modes
nim-serde offers three modes to control which fields are serialized:
import pkg/serde/json
# OptIn mode (default): Only fields with {.serialize.} are included
type Person1 = object
name {.serialize.}: string
age {.serialize.}: int
address: string # Not serialized
assert Person1(name: "John", age: 30, address: "123 Main St").toJson == """{"name":"John","age":30}"""
# OptOut mode: All fields are included except those marked to ignore
type Person2 {.serialize.} = object
name: string
age: int
ssn {.serialize(ignore=true).}: string # Not serialized
assert Person2(name: "John", age: 30, ssn: "123-45-6789").toJson == """{"name":"John","age":30}"""
# Strict mode: All fields are included, and an error is raised if any fields are missing
type Person3 {.serialize(mode=Strict).} = object
name: string
age: int
assert Person3(name: "John", age: 30).toJson == """{"name":"John","age":30}"""
Field Customization for Serialization
Fields can be customized with various options:
import pkg/serde/json
# Field customization for serialization
type Person {.serialize(mode = OptOut).} = object
firstName {.serialize(key = "first_name").}: string
lastName {.serialize(key = "last_name").}: string
age: int # Will be included because we're using OptOut mode
ssn {.serialize(ignore = true).}: string # Sensitive data not serialized
let person = Person(
firstName: "John",
lastName: "Doe",
age: 30,
ssn: "123-45-6789"
)
let jsonNode = %person
assert jsonNode.kind == JObject
assert jsonNode["first_name"].getStr == "John"
assert jsonNode["last_name"].getStr == "Doe"
assert jsonNode["age"].getInt == 30
assert "ssn" notin jsonNode
# Convert to JSON string
let jsonStr = toJson(person)
assert jsonStr == """{"first_name":"John","last_name":"Doe","age":30}"""
Deserialization API
nim-serde provides a type-safe way to convert JSON data back into Nim types.
Basic Deserialization with fromJson
The fromJson function converts JSON strings or JsonNode objects to Nim types:
import pkg/serde/json
import pkg/questionable/results
type Person = object
name: string
age: int
let jsonStr = """{"name":"John","age":30}"""
let result = Person.fromJson(jsonStr)
# Using the ! operator from questionable to extract the value
assert !result == Person(name: "John", age: 30)
Error Handling
Deserialization returns a Result type from the questionable library, allowing for safe error handling:
import pkg/serde/json
import pkg/questionable/results
type Person = object
name: string
age: int
let invalidJson = """{"name":"John","age":"thirty"}"""
let errorResult = Person.fromJson(invalidJson)
assert errorResult.isFailure
assert errorResult.error of UnexpectedKindError
Parsing JSON with JsonNode.parse
For parsing raw JSON without immediate deserialization:
import pkg/serde/json
import pkg/questionable/results
let jsonStr = """{"name":"John","age":30,"hobbies":["reading","coding"]}"""
let parseResult = JsonNode.parse(jsonStr)
assert parseResult.isSuccess
let jsonNode = !parseResult
assert jsonNode["name"].getStr == "John"
assert jsonNode["age"].getInt == 30
assert jsonNode["hobbies"].kind == JArray
assert jsonNode["hobbies"][0].getStr == "reading"
assert jsonNode["hobbies"][1].getStr == "coding"
Deserialization Modes
nim-serde offers three modes to control how JSON is deserialized:
import pkg/serde/json
import pkg/questionable/results
# OptOut mode (default for deserialization)
type PersonOptOut = object
name: string
age: int
let jsonOptOut = """{"name":"John","age":30,"address":"123 Main St"}"""
let resultOptOut = PersonOptOut.fromJson(jsonOptOut)
assert resultOptOut.isSuccess
assert !resultOptOut == PersonOptOut(name: "John", age: 30)
# OptIn mode
type PersonOptIn {.deserialize(mode = OptIn).} = object
name {.deserialize.}: string
age {.deserialize.}: int
address: string # Not deserialized by default in OptIn mode
let jsonOptIn = """{"name":"John","age":30,"address":"123 Main St"}"""
let resultOptIn = PersonOptIn.fromJson(jsonOptIn)
assert resultOptIn.isSuccess
assert (!resultOptIn).name == "John"
assert (!resultOptIn).age == 30
assert (!resultOptIn).address == "" # address is not deserialized
# Strict mode
type PersonStrict {.deserialize(mode = Strict).} = object
name: string
age: int
let jsonStrict = """{"name":"John","age":30}"""
let resultStrict = PersonStrict.fromJson(jsonStrict)
assert resultStrict.isSuccess
assert !resultStrict == PersonStrict(name: "John", age: 30)
# Strict mode with extra field (should fail)
let jsonStrictExtra = """{"name":"John","age":30,"address":"123 Main St"}"""
let resultStrictExtra = PersonStrict.fromJson(jsonStrictExtra)
assert resultStrictExtra.isFailure
Field Customization for Deserialization
Fields can be customized with various options for deserialization:
import pkg/serde/json
import pkg/questionable/results
type User = object
firstName {.deserialize(key = "first_name").}: string
lastName {.deserialize(key = "last_name").}: string
age: int
internalId {.deserialize(ignore = true).}: int
let userJsonStr = """{"first_name":"Jane","last_name":"Smith","age":25,"role":"admin"}"""
let result = User.fromJson(userJsonStr)
assert result.isSuccess
assert (!result).firstName == "Jane"
assert (!result).lastName == "Smith"
assert (!result).age == 25
assert (!result).internalId == 0 # Default value, not deserialized
Using as a Drop-in Replacement for std/json
nim-serde can be used as a drop-in replacement for the standard library's json module with improved exception handling.
# Instead of:
# import std/json
import pkg/serde/json
import pkg/questionable/results
# Using nim-serde's JSON API which is compatible with std/json
let jsonNode = %* {
"name": "John",
"age": 30,
"isActive": true,
"hobbies": ["reading", "swimming"]
}
# Accessing JSON fields using the same API as std/json
assert jsonNode.kind == JObject
assert jsonNode["name"].getStr == "John"
assert jsonNode["age"].getInt == 30
assert jsonNode["isActive"].getBool == true
assert jsonNode["hobbies"].kind == JArray
assert jsonNode["hobbies"][0].getStr == "reading"
assert jsonNode["hobbies"][1].getStr == "swimming"
# Converting JSON to string
let jsonStr = $jsonNode
# Parsing JSON from string with better error handling
let parsedResult = JsonNode.parse(jsonStr)
assert parsedResult.isSuccess
let parsedNode = !parsedResult
assert parsedNode.kind == JObject
assert parsedNode["name"].getStr == "John"
# Pretty printing
let prettyJson = pretty(jsonNode)
Custom Type Serialization
You can extend nim-serde to support custom types by defining your own % operator overloads and fromJson procs:
import pkg/serde/json
import pkg/serde/utils/errors
import pkg/questionable/results
import std/strutils
# Define a custom type
type
UserId = distinct int
# Custom serialization for UserId
proc `%`*(id: UserId): JsonNode =
%("user-" & $int(id))
# Custom deserialization for UserId
proc fromJson*(_: type UserId, json: JsonNode): ?!UserId =
if json.kind != JString:
return failure(newSerdeError("Expected string for UserId, got " & $json.kind))
let str = json.getStr()
if str.startsWith("user-"):
let idStr = str[5..^1]
try:
let id = parseInt(idStr)
success(UserId(id))
except ValueError:
failure(newSerdeError("Invalid UserId format: " & str))
else:
failure(newSerdeError("UserId must start with 'user-' prefix"))
# Test serialization
let userId = UserId(42)
let jsonNode = %userId
assert jsonNode.kind == JString
assert jsonNode.getStr() == "user-42"
# Test deserialization
let jsonStr = "\"user-42\""
let parsedJson = !JsonNode.parse(jsonStr)
let result = UserId.fromJson(parsedJson)
assert result.isSuccess
assert int(!result) == 42
# Test in object context
type User {.serialize(mode = OptOut).} = object
id: UserId
name: string
let user = User(id: UserId(123), name: "John")
let userJson = %user
assert userJson.kind == JObject
assert userJson["id"].getStr() == "user-123"
assert userJson["name"].getStr() == "John"
# Test deserialization of object with custom type
let userJsonStr = """{"id":"user-123","name":"John"}"""
let userResult = User.fromJson(userJsonStr)
assert userResult.isSuccess
assert int((!userResult).id) == 123
assert (!userResult).name == "John"
Implementation Details
The JSON serialization in nim-serde is based on the % operator pattern:
- The
%operator converts values toJsonNodeobjects - Various overloads handle different types (primitives, objects, collections)
- The
toJsonfunction converts theJsonNodeto a string
This approach makes it easy to extend with custom types by defining your own % operator overloads.
For deserialization, the library uses compile-time reflection to map JSON fields to object fields, respecting the configuration provided by pragmas.