lens

Typed JSON lenses, builders, and aggregate validation for MoonBit

json
lens
validation
moonbit
moon add totto2727/lens@0.4.2
Download zip
Author
Version
0.4.2
License
MIT
Last updated
5 days ago
Downloads
413
README

#lens

Reusable typed access, construction, and aggregate validation for MoonBit JSON values.

The package keeps static types on Lens[T]. Applying nullable, optional, or nullish produces PresenceLens[T], whose reads and writes use T? without nesting options. ObjectLens reads selected objects as Map[String, Json]. The returned map is copied so top-level mutations do not change the source document; nested Json values retain their normal sharing semantics. A JsonBuilder accepts typed values through the same lenses and implements ToJson. Validation only reports whether every requested read succeeds; it does not infer or construct application types from runtime definitions. After successful validation, read values through the original lenses.

#Typed access

test {
let document = @json.parse(
"{\"user\":{\"name\":\"Ada\",\"age\":37,\"active\":true}}",
)
let user = object("user")
let name_lens = user.string("name")
let age_lens = user.int("age")
let active_lens = user.bool("active")

inspect(name_lens.get(document), content="Ada")
inspect(age_lens.get(document), content="37")
inspect(active_lens.get(document), content="true")
}

Lens::get raises LensError(Issue) when traversal or decoding fails. Each Issue contains an RFC 6901 pointer, a structured IssueCode, and optional diagnostic context.

test {
let document = @json.parse("{\"user\":{}}")
try {
object("user").string("name").get(document) |> ignore
fail("expected LensError")
} catch {
LensError(issue) =>
inspect(issue.pointer.to_string(), content="/user/name")
_ => fail("unexpected error")
}
}

A custom lens can read and write any type implementing the standard FromJson and ToJson traits. get_or_json_decode_error preserves the selected path in nested decode errors, and set delegates serialization to ToJson.

struct Repository {
owner : String
} derive(FromJson, ToJson)

fn decode_repository(
document : Json,
path : @json.JsonPath,
) -> Repository raise @json.JsonDecodeError {
let repository : Lens[Repository] = root().custom("repository")
repository.get_or_json_decode_error(document, path)
}

Use ObjectLens::json when the selected value must remain raw Json. Use ObjectLens::custom when the application type owns both its standard JSON decoding and encoding contract. Lens[Json]::decode_from_json remains available for read-only types that implement FromJson but not ToJson.

#Typed construction

JsonBuilder constructs an output object without requiring an existing Json document. Lens::set encodes its typed value, creates missing object parents, and writes it at the lens pointer. Repeated writes to the same pointer use the latest value.

test {
let builder = JsonBuilder::JsonBuilder()
let user = object("user")
user.string("name").set(builder, "Ada")
user.int("age").set(builder, 37)
user.bool("active").set(builder, true)
user.string("roles").array().set(builder, ["admin", "reviewer"])

@json.json_inspect(builder, content={
"user": {
"name": "Ada",
"age": 37,
"active": true,
"roles": ["admin", "reviewer"],
},
})
}

nullable(None) writes JSON null, while optional(None) omits the property and removes a previous value at the same pointer. nullish(None) omits the property by default; pass encode_mode=NullishEncodeMode::Null to write JSON null instead. Repeated presence combinators use the last call, so both optional().nullable() and nullish().nullable() have nullable semantics: None writes JSON null, and a missing property remains an error. Array items cannot be omitted without changing later indices, so PresenceLens::array() always treats its items as nullable for both encoding and decoding.

Lens::set raises JsonBuildError(JsonBuildIssue) when an encoded leaf blocks a nested object path. The issue contains the exact output pointer and a structured JsonBuildIssueCode. The builder is unchanged when construction fails.

#Arrays and presence

Compose an item lens with array to decode every array element. Item failures include their zero-based index in the reported JSON Pointer.

test {
let document = @json.parse("{\"names\":[\"Ada\",\"Grace\"]}")
debug_inspect(
root().string("names").array().get(document),
content="[\"Ada\", \"Grace\"]",
)
}

nullable, optional, and nullish keep missing properties distinct from explicit JSON null:

Input stateRequired stringnullableoptionalnullish
MissingerrorerrorNoneNone
nulltype errorNonetype errorNone
StringvalueSome(T)Some(T)Some(T)

Here, missing means that the selected leaf property is absent. A missing intermediate object remains an error so optional values cannot hide an invalid surrounding structure.

PresenceLens::array() is a normalization boundary: regardless of whether it was created from optional, nullable, or nullish, every item uses nullable semantics. JSON null decodes to None, and None encodes as JSON null, preserving array indices and matching JavaScript JSON array serialization. This differs from primitive.array().optional(), where optional applies to the whole array property rather than its items.

Presence combinators can be replaced without changing the value type. Each call returns PresenceLens[T], and the final call determines both decoding and encoding behavior. For example, nullable().optional() accepts a missing property but rejects JSON null, while optional().nullable() rejects a missing property but accepts and writes JSON null.

#Validation

LensTrait exposes only the type-erased check operation required by aggregate validation. Lens[T], PresenceLens[T], and ObjectLens implement it, so heterogeneous lenses can be passed directly to validate. Every check runs, and failures are returned in input order.

test {
let document = @json.parse(
"{\"user\":{\"name\":\"Ada\",\"age\":37}}",
)
let user = object("user")
let name_lens = user.string("name")
let age_lens = user.int("age")

match validate(document, [user, name_lens, age_lens]) {
Valid => {
let name : String = name_lens.get(document)
let age : Int = age_lens.get(document)
inspect((name, age), content="(Ada, 37)")
}
Invalid(issues) => fail("unexpected issues: \{issues}")
}
}

Validation::Valid carries no decoded value. Validation::Invalid carries only Array[Issue].

#Numeric behavior

number returns the existing Double stored in Json::Number without further validation, including non-finite values. int delegates directly to MoonBit's standard Double::to_int conversion without package-level validation, inheriting its truncation, saturation, and special-value behavior. The package never reparses the retained JSON number text.

#Current scope

The API supports object-property traversal and builder construction; String, Bool, Double, standard-converted Int, and raw Json values; typed arrays; and nullable, optional, and nullish values. Lens::set writes to JsonBuilder; it does not mutate or copy an existing JSON document. Refinements, alternatives, and source-document mutation remain outside the current scope.

See the design document for the detailed contract and roadmap. A Japanese translation is available at docs/design.ja.md.

#
LensTrait

pub trait LensTrait {
fn check(Self, Json) -> Unit raise LensError
}

A type-erased lens read used only for aggregate validation.

#
JsonBuildError

pub suberror JsonBuildError {
JsonBuildError(JsonBuildIssue)
} derive(Eq,
Debug
)

The typed error raised when a lens cannot write a value to a JSON builder.

#
LensError

pub suberror LensError {
LensError(Issue)
} derive(Eq,
Debug
)

The typed error raised when a lens cannot read its selected value.

#
Decoder

type Decoder[T]

#
Encoder

type Encoder[T]

#
Issue

pub struct Issue {
pointer : Pointer
code : IssueCode
message : String?
} derive(Eq,
Debug
)

A structured diagnostic containing the exact pointer and failure reason.

#
IssueCode

pub enum IssueCode {
MissingProperty
TypeMismatch(expected~ : JsonKind, actual~ : JsonKind)
IndexOutOfBounds(index~ : Int, length~ : Int)
ConstraintViolation(code~ : String)
ExternalDecode
} derive(Eq,
Debug
)

A machine-readable reason why a lens or validation check failed.

#
JsonBuildIssue

pub struct JsonBuildIssue {
pointer : Pointer
code : JsonBuildIssueCode
} derive(Eq,
Debug
)

A structured JSON construction failure at an exact output pointer.

#
JsonBuildIssueCode

pub enum JsonBuildIssueCode {
PathConflict
} derive(Eq,
Debug
)

A machine-readable reason why a typed value could not be written to a JSON builder.

#
JsonBuilder

pub struct JsonBuilder {
// private fields
}

A mutable object builder populated through typed lenses and serialized with ToJson.

#
JsonBuilder::JsonBuilder

fn JsonBuilder::JsonBuilder() -> JsonBuilder

Creates an empty JSON object builder.

#
JsonKind

pub enum JsonKind {
Null
Boolean
Number
String
Array
Object
} derive(Eq,
Debug
)

The JSON value category used by structured lens diagnostics.

#
Lens

pub struct Lens[T] {
// private fields
}

A reusable typed accessor for one location in a JSON document.
impl LensTrait for Lens[T]

#
Lens::add_to_json_path

Appends this lens's pointer to an existing JSON path.

#
Lens::array

fn[T] Lens::array(self : Lens[T]) -> Lens[Array[T]]

Returns a lens that decodes every array item with this lens's decoder.

#
Lens::decode_from_json

Selects raw JSON and delegates read-only decoding to standard FromJson.

#
Lens::get

fn[T] Lens::get(self : Lens[T], document : Json) -> T raise LensError

Reads and decodes this lens from a JSON document.

#
Lens::get_or_json_decode_error

fn[T] Lens::get_or_json_decode_error(self : Lens[T], document : Json, path :
JsonPath
) -> T raise
JsonDecodeError

Reads this lens and translates a lens failure into a standard JSON decode error.

#
Lens::json_decode_error

fn[T] Lens::json_decode_error(self : Lens[T], path :
JsonPath
, message : String) ->
JsonDecodeError

Creates a standard JSON decode error at this lens's location under an existing path.

#
Lens::nullable

fn[T] Lens::nullable(self : Lens[T]) -> PresenceLens[T]

Returns None for JSON null while preserving missing-property errors.

#
Lens::nullish

fn[T] Lens::nullish(self : Lens[T], encode_mode? : NullishEncodeMode) -> PresenceLens[T]

Returns None for either a missing selected path or JSON null.

None is omitted by default. Pass encode_mode=Null to write JSON null instead.

#
Lens::optional

fn[T] Lens::optional(self : Lens[T]) -> PresenceLens[T]

Returns None for a missing selected path while rejecting JSON null.

#
Lens::set

fn[T] Lens::set(self : Lens[T], builder : JsonBuilder, value : T) -> Unit raise JsonBuildError

Writes a typed value at this lens's output pointer.

Repeated writes to the same pointer use the latest value. Missing object parents are created. An omitted optional value removes a previous value and prunes empty generated parents.

#
Lens::set_or_abort

fn[T] Lens::set_or_abort(self : Lens[T], builder : JsonBuilder, value : T) -> Unit

Writes a typed value for an infallible serialization contract.

Use this from ToJson::to_json, whose trait signature cannot propagate JsonBuildError. A failure indicates a conflicting static output schema or another serializer implementation defect and aborts the process.

#
NullishEncodeMode

pub(all) enum NullishEncodeMode {
Omit
Null
} derive(Eq,
Debug
)

Controls how a nullish lens encodes None.

#
ObjectLens

pub struct ObjectLens {
// private fields
}

A location in a JSON document from which typed child lenses can be created.

#
ObjectLens::bool

fn ObjectLens::bool(self : ObjectLens, key : String) -> Lens[Bool]

Returns a boolean lens positioned at a child property.

#
ObjectLens::custom

fn[T :
FromJson
+ ToJson] ObjectLens::custom(self : ObjectLens, key : String) -> Lens[T]

Returns a typed lens that delegates decoding and encoding to standard JSON traits.

#
ObjectLens::get

fn ObjectLens::get(self : ObjectLens, document : Json) -> Map[String, Json] raise LensError

Reads this object lens from a JSON document.

#
ObjectLens::int

fn ObjectLens::int(self : ObjectLens, key : String) -> Lens[Int]

Returns an integer lens that delegates conversion to Double::to_int.

#
ObjectLens::json

fn ObjectLens::json(self : ObjectLens, key : String) -> Lens[Json]

Returns a raw JSON lens positioned at a child property.

#
ObjectLens::number

fn ObjectLens::number(self : ObjectLens, key : String) -> Lens[Double]

Returns a finite-number lens positioned at a child property.

#
ObjectLens::object

fn ObjectLens::object(self : ObjectLens, key : String) -> ObjectLens

Returns an object lens positioned at a child property.

#
ObjectLens::string

fn ObjectLens::string(self : ObjectLens, key : String) -> Lens[String]

Returns a string lens positioned at a child property.

#
Pointer

pub struct Pointer {
// private fields
} derive(Eq,
Debug
)

An immutable JSON Pointer identifying a location in a JSON document.

#
Pointer::to_string

fn Pointer::to_string(self : Pointer) -> String

Returns the RFC 6901 string representation of this pointer.

#
PresenceLens

pub struct PresenceLens[T] {
// private fields
}

A typed accessor whose missing and null behavior can be replaced without nesting options.

#
PresenceLens::add_to_json_path

Appends this lens's pointer to an existing JSON path.

#
PresenceLens::array

fn[T] PresenceLens::array(self : PresenceLens[T]) -> Lens[Array[T?]]

Returns a lens that treats every array item as nullable.

#
PresenceLens::get

fn[T] PresenceLens::get(self : PresenceLens[T], document : Json) -> T? raise LensError

Reads and decodes this presence-aware lens from a JSON document.

#
PresenceLens::get_or_json_decode_error

Reads this lens and translates a lens failure into a standard JSON decode error.

#
PresenceLens::json_decode_error

Creates a standard JSON decode error at this lens's location under an existing path.

#
PresenceLens::nullable

fn[T] PresenceLens::nullable(self : PresenceLens[T]) -> PresenceLens[T]

Replaces the current presence policy with nullable semantics.

#
PresenceLens::nullish

fn[T] PresenceLens::nullish(self : PresenceLens[T], encode_mode? : NullishEncodeMode) -> PresenceLens[T]

Replaces the current presence policy with nullish semantics.

#
PresenceLens::optional

fn[T] PresenceLens::optional(self : PresenceLens[T]) -> PresenceLens[T]

Replaces the current presence policy with optional semantics.

#
PresenceLens::set

fn[T] PresenceLens::set(self : PresenceLens[T], builder : JsonBuilder, value : T?) -> Unit raise JsonBuildError

Writes an optional typed value using this lens's current presence policy.

#
PresenceLens::set_or_abort

fn[T] PresenceLens::set_or_abort(self : PresenceLens[T], builder : JsonBuilder, value : T?) -> Unit

Writes an optional typed value for an infallible serialization contract.

#
Validation

pub enum Validation {
Valid
Invalid(ReadOnlyArray[Issue])
} derive(Eq,
Debug
)

The outcome of validating a JSON document with type-erased checks.

Valid carries no decoded value. Use the original typed lenses to read the document after validation succeeds.

#
object

fn object(key : String) -> ObjectLens

Returns an object lens positioned at a root property.

#
root

fn root() -> ObjectLens

Returns an object lens positioned at the document root.

#
validate

fn validate(document : Json, lenses : Array[&LensTrait]) -> Validation

Runs every lens check and returns all issues in input order.

This function never constructs or returns application values.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io