A lightweight JSON parsing library for Mojo.
Use the parse function to parse a JSON value from a string. It accepts a
ParseOptions struct as a parameter to alter parsing behaviour.
from emberjson import parse, ParseOptions
# Use custom options
var json = parse[ParseOptions(ignore_unicode=True)](r'["\uD83D\uDD25"]')EmberJSON supports decoding escaped unicode characters.
print(parse(r'["\uD83D\uDD25"]')) # prints '["🔥"]'Use try_parse for a non-raising variant that returns an Optional[Value]:
from emberjson import try_parse
var result = try_parse('{"key": 123}')
if result:
print(result.value()) # prints {"key":123}parse_document parses onto an immutable, self-contained tape Document
(simdjson-style): a SIMD structural index feeds an iterative stage-2
walker, containers are tape spans, and all strings share one arena — no
per-node allocation and no byte-at-a-time scanning. That makes it 2-3x
faster than parse across the bench corpus. The trade-off: the result is read-only, and navigation values
borrow the Document (enforced at compile time by the origin system).
from emberjson import parse_document, to_string
def main() raises:
var d = parse_document('{"name": "ember", "versions": [1, 2.5]}')
print(d.root()["name"].string()) # ember
print(d.root()["versions"][1].float()) # 2.5
print(len(d.root()["versions"])) # 2
for entry in d.root().object(): # zero-copy keys and values
print(entry.key)
print(to_string(d)) # serialize straight off the tape
# Materialize an owned, mutable Value tree when you need to modify
# the data or keep it beyond the Document's lifetime:
var v = d.to_value()
v["name"] = "modified"try_parse_document is the non-raising variant returning an
Optional[Document].
When you need one value out of a large document, parse_pointer navigates
the raw text to an RFC 6901 target using a SIMD structural index and
parses only that subtree — sibling values are skipped with bracket hops,
never visiting their contents. Sparse queries run 3-6x faster than even
parse_document on the bench corpus (5-19x faster than parse).
from emberjson import parse_pointer
var name = parse_pointer(big_doc, "/statuses/99/user/screen_name")
print(name.string())The trade-off: the target (and everything actually traversed) is fully
validated, but bytes that are merely skipped over are checked only for
structural sanity — parse_pointer('{"bad": nope, "good": 1}', "/good")
succeeds. Use parse when whole-document validation matters.
try_parse_pointer is the non-raising variant.
JSON text is required to be UTF-8 (RFC 8259), and every parse entry point validates that by default: input containing overlongs, surrogates, truncated sequences, or code points above U+10FFFF is rejected before parsing. The check is a SIMD validator running at ~20-30 GB/s (with an ASCII fast path), so it typically costs 2-4% of a parse. For trusted input you can opt out:
from emberjson import parse, is_valid_utf8, ParseOptions
var v = parse(untrusted_bytes) # raises on invalid UTF-8
comptime trusted = ParseOptions(validate_utf8=False)
var w = parse[trusted](my_own_bytes) # skips the check
# Also available standalone:
if is_valid_utf8(some_bytes):
...Use the to_string function to convert a JSON struct to its string representation.
It accepts a parameter to control whether to pretty print the value.
The JSON struct also conforms to the Writable trait.
from emberjson import parse, to_string
def main() raises:
var json = parse('{"key": 123}')
print(to_string(json)) # prints {"key":123}
print(to_string[pretty=True](json))
# prints:
#{
# "key": 123
#}Use minify to strip whitespace from a JSON string without parsing:
from emberjson import minify
var compact = minify('{ "key" : 123 }')
print(compact) # prints {"key":123}Value is the unified type for any JSON value. It can represent
an Object, Array, String, Int, Float64, Bool, or Null.
from emberjson import *
var json = parse('{"key": 123}')
# check inner type
print(json.is_object()) # prints True
# dict style access
print(json.object()["key"].int()) # prints 123
# array
var value = parse('[123, 4.5, "string", true, null]')
ref array = value.array()
# array style access
print(array[3].bool()) # prints True
# equality checks
print(array[4] == Null()) # prints True
# None converts implicitly to Null
assert_equal(array[4], Value(None))
# Implicit ctors for Value
var v: Value = "some string"
# Convert Array and Dict back to stdlib types
# These are consuming actions so the original Array/Object will be moved
var arr = Array(123, False)
var l = arr.to_list()
var ob = Object()
var d = ob.to_dict()Using Mojo's reflection features, EmberJson can automatically serialize and deserialize JSON to and from Mojo structs without propagating trait implementations for all relevant types. Plain structs are treated as JSON objects by default. The logic recursively traverses struct fields until it finds conforming types, so nested structs work out of the box.
Supported field types include: Int, Float64, String, Bool, Optional[T], List[T], Dict[String, V], Tuple[...], Set[T], InlineArray[T, N], SIMD[dtype, size], ArcPointer[T], OwnedPointer[T], and nested structs.
To customize behavior, implement the JsonSerializable and/or JsonDeserializable traits.
The target struct must implement the Movable trait.
As well as the Defaultable trait if any of its fields
have non-trivial destructors.
from emberjson import deserialize, try_deserialize
@fieldwise_init
struct User(Defaultable, Movable):
var id: Int
var name: String
var is_active: Bool
var scores: List[Float64]
def __init__(out self):
self.id = 0
self.name = ""
self.is_active = False
self.scores = List[Float64]()
def main() raises:
var json_str = '{"id": 1, "name": "Mojo", "is_active": true, "scores": [9.9, 8.5]}'
# Raises on invalid JSON
var user = deserialize[User](json_str)
# Returns Optional[User] instead of raising
var user_opt = try_deserialize[User](json_str)
if user_opt:
print(user_opt.value().name) # prints MojoNested structs and Optional fields are handled automatically. Missing JSON keys for Optional fields default to None:
@fieldwise_init
struct Address(Defaultable, Movable):
var city: String
var zip: Optional[String]
def __init__(out self):
self.city = ""
self.zip = None
@fieldwise_init
struct Person(Defaultable, Movable):
var name: String
var address: Address
def __init__(out self):
self.name = ""
self.address = Address()
def main() raises:
var json_str = '{"name": "Mojo", "address": {"city": "SF"}}'
var person = deserialize[Person](json_str)
print(person.name) # prints Mojo
print(person.address.city) # prints SF
print(person.address.zip) # prints None (missing field)Implement JsonDeserializable to deserialize from a JSON array instead of an object:
@fieldwise_init
struct Point(JsonDeserializable):
var x: Int
var y: Int
@staticmethod
def deserialize_as_array() -> Bool:
return True
def main() raises:
var p = deserialize[Point]("[1, 2]")
print(p.x) # prints 1
print(p.y) # prints 2from emberjson import *
@fieldwise_init
struct Point:
var x: Int
var y: Int
def main():
print(serialize(Point(1, 2))) # prints {"x":1,"y":2}
print(serialize[pretty=True](Point(1, 2))) # pretty printedImplement JsonSerializable to control how a struct is serialized:
# Serialize as a JSON array instead of an object
@fieldwise_init
struct Coordinate(JsonSerializable):
var lat: Float64
var lng: Float64
@staticmethod
def serialize_as_array() -> Bool:
return True
# Fully custom serialization via write_json
@fieldwise_init
struct MyInt(JsonSerializable):
var value: Int
def write_json(self, mut writer: Some[Serializer]):
writer.write(self.value)
def main():
print(serialize(Coordinate(1.0, 2.0))) # prints [1.0,2.0]
print(serialize(MyInt(1))) # prints 1Any deserialized struct can defer expensive subtrees by declaring fields
as Lazy wrappers (LazyValue, LazyString, LazyInt, LazyFloat,
or Lazy[YourType, origin]): during deserialize those fields only
record their byte span (grammar-validated, so re-serialization is safe),
and materialize when you call .get(). The struct is parameterized on
the input's origin, which lets the compiler guarantee the spans cannot
outlive the source string.
from emberjson import deserialize, LazyValue, LazyString
struct Event[origin: ImmutOrigin](Movable):
var id: Int64 # parsed eagerly
var payload: LazyValue[Self.origin] # span captured, parsed on demand
var note: LazyString[Self.origin]
var e = deserialize[Event[origin_of(data)]](data)
print(e.id) # already materialized
print(e.payload.get()) # parses just this subtree, nowEmberJson provides compile-time schema validation types that enforce constraints during both construction and deserialization. Validators wrap a value and raise on constraint violations. All validators integrate with serialize/deserialize and can be used as struct field types.
Access the validated value with []:
from emberjson import *
var port = Range[Int, 1, 65535](8080)
print(port[]) # prints 8080
var port2 = deserialize[Range[Int, 1, 65535]]("443")
print(port2[]) # prints 443| Validator | Description | Example |
|---|---|---|
Range[T, min, max] |
Inclusive range (min <= value <= max) |
Range[Int, 0, 100] |
ExclusiveRange[T, min, max] |
Exclusive range (min < value < max) |
ExclusiveRange[Float64, 0.0, 1.0] |
Size[T, min, max] |
Length/size constraint | Size[String, 1, 255] |
NonEmpty[T] |
Non-empty check | NonEmpty[List[Int]] |
StartsWith[prefix] |
String prefix check | StartsWith["https://"] |
EndsWith[suffix] |
String suffix check | EndsWith[".json"] |
Eq[value] |
Equality check | Eq[42] |
Ne[value] |
Inequality check | Ne["forbidden"] |
MultipleOf[base] |
Divisibility check | MultipleOf[Int64(10)] |
Unique[T] |
All elements unique | Unique[List[Int]] |
Enum[T, *values] |
Set membership | Enum[String, "red", "green", "blue"] |
from emberjson import *
# Validate on deserialization
var name = deserialize[NonEmpty[String]]('"Alice"')
# Validate on construction
var score = Range[Float64, 0.0, 100.0](95.5)
# Enum-style validation
comptime Color = Enum[String, "red", "green", "blue"]
var c = deserialize[Color]('"red"')
print(c[]) # prints redCombine validators for complex constraints:
from emberjson import *
# AllOf: ALL validators must pass
var v = deserialize[
AllOf[String, Size[String, 3, 7], StartsWith["a"]]
]('"astring"')
# OneOf: EXACTLY one validator must pass
var o = deserialize[
OneOf[String, Eq["red"], Eq["green"], Eq["blue"]]
]('"red"')
# AnyOf: AT LEAST one validator must pass
var a = deserialize[
AnyOf[Int, Eq[1], Eq[2], Range[Int, 10, 20]]
]("15")
# NoneOf: NO validators must pass
var n = deserialize[
NoneOf[Int, Range[Int, 0, 5], Eq[100]]
]("7")
# Not: invert any validator
var x = deserialize[Not[Int, Range[Int, 0, 10]]]("15")Transformers modify values during deserialization or serialization:
from emberjson import *
# Default: use a fallback value when the field is missing or null
var d = deserialize[Default[Int, 42]]("null")
print(d[]) # prints 42
# Secret: deserializes normally, serializes as "********"
var pw = deserialize[Secret[String]]('"my_password"')
print(pw[]) # prints my_password
print(serialize(pw)) # prints "********"
# Clamp: constrains value to a range instead of rejecting
var c = deserialize[Clamp[Int, 0, 100]]("150")
print(c[]) # prints 100 (clamped to max)
# CoerceInt/CoerceFloat/CoerceString: type coercion from JSON
var i = deserialize[CoerceInt]('"123"')
print(i[]) # prints 123 (coerced from string)
# Transform: apply a function during deserialization
def date_to_epoch(s: String) -> Int:
if s == "2024-01-01":
return 1704067200
return 0
var epoch = deserialize[Transform[String, Int, date_to_epoch]]('"2024-01-01"')
print(epoch[]) # prints 1704067200Validators work as struct field types, enforcing constraints during deserialization:
from emberjson import *
@fieldwise_init
struct Config(Defaultable, Movable):
var name: NonEmpty[String]
var port: Range[Int, 1, 65535]
var timeout: Default[Int, 30]
var password: Secret[String]
def __init__(out self):
self.name = "default"
self.port = 80
self.timeout = Default[Int, 30]()
self.password = ""
def main() raises:
var cfg = deserialize[Config](
'{"name": "myapp", "port": 8080, "password": "s3cret"}'
)
print(cfg.name[]) # prints myapp
print(cfg.port[]) # prints 8080
print(cfg.timeout[]) # prints 30 (default, since missing from JSON)
print(serialize(cfg)) # password serialized as "********"Validate relationships between fields of a struct:
from emberjson import *
from emberjson.schema import CrossFieldValidator
@fieldwise_init
struct DateRange(Defaultable, Movable):
var start: Int
var end: Int
def __init__(out self):
self.start = 0
self.end = 0
def validate_order(start: Int, end: Int) raises:
if start >= end:
raise Error("start must be before end")
def main() raises:
var dr = deserialize[
CrossFieldValidator[DateRange, "start", "end", validate_order]
]('{"start": 1, "end": 10}')
print(dr[].start) # prints 1
print(dr[].end) # prints 10EmberJSON supports RFC 6901 JSON Pointer for traversing documents with a string path.
The get() method works on Value types and returns a reference
to the nested value. It also supports syntactic sugar via backticks.
var j = Value(parse_string='{"foo": ["bar", "baz"]}')
# Access nested values
print(j.get("/foo/1").string()) # prints "baz"
# Syntactic sugar via backticks
print(j.`/foo/1`.string())
# Modify values
j.get("/foo/1") = "modified"
# or
j.`/foo/1` = "modified"
# RFC 6901 Escaping (~1 for /, ~0 for ~) covers special characters
var j2 = Value(parse_string='{"a/b": 1, "m~n": 2}')
print(j2.get("/a~1b").int()) # prints 1
print(j2.get("/m~0n").int()) # prints 2You can also use Python-style dot access for object keys, or backtick-identifiers for full paths:
# Dot access for standard identifiers
print(j.foo) # Equivalent to j.pointer("/foo")
# Backtick syntax for full pointer paths
print(j.`/foo/1`.string()) # Equivalent to j.pointer("/foo/1")
# In-place modification via backticks
j.`/foo/1` = "updated"
print(j.`/foo/1`.string()) # prints "updated"
# Chained access for nest objects
j = {"foo": {"bar": [1, 2, 3]}}
print(j.foo.bar[1]) # prints "2"EmberJson supports RFC 6902 JSON Patch for applying a sequence of operations to a JSON document, and RFC 7386 JSON Merge Patch for recursive merging.
from emberjson import parse, Value, Object
from emberjson.patch import patch, merge_patch
def main() raises:
# RFC 6902: apply a sequence of operations
var doc = parse('{"foo": "bar", "items": [1, 2]}')
patch(doc, """[
{"op": "replace", "path": "/foo", "value": "baz"},
{"op": "add", "path": "/items/-", "value": 3},
{"op": "remove", "path": "/items/0"}
]""")
# doc is now {"foo": "baz", "items": [2, 3]}
# Supported operations: add, remove, replace, move, copy, test
# "test" asserts a value matches — raises if it doesn't
patch(doc, '[{"op": "test", "path": "/foo", "value": "baz"}]')
# RFC 7386: recursive merge patch
var target = parse('{"a": "b", "c": {"d": "e", "f": "g"}}')
merge_patch(target, '{"a": "z", "c": {"f": null}}')
# target is now {"a": "z", "c": {"d": "e"}}
# null values remove keysRead and write JSON Lines files (one JSON value per line):
from emberjson import read_lines, write_lines, Value, Array
from std.pathlib import Path
def main() raises:
# Read: iterate over lines lazily
for value in read_lines("data.jsonl"):
print(value)
# Read: collect all lines into a list
var all_values = read_lines("data.jsonl").collect()
# Write: save a list of values as JSONL
var lines: List[Value] = [Value(1), Value(2), Value(3)]
write_lines(Path("output.jsonl"), lines)EmberJson uses the Teju Jagua algorithm for efficient floating-point formatting, developed by Cassio Neri and licensed under the Apache License, Version 2.0.
