EmberJson: JSON parsing in pure mojo

EmberJson 0.3.1

EmberJson 0.3.1 (Mojo 0.26.2) is now available from the Mojo community pixi channel.

Whats new

Lazy structured parsing

Data validation features in structured parsing

The schema module now includes a selection of different utility types to add additional power
to the existing reflection based structured parsing features.

Validators

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"]

Combine 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")

Data Transformers

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 1704067200

Cross-Field Validation

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 10

Using Validators in Structs

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 "********"
2 Likes