nupp.codec.json
nupp.codec.json is a strict Nupp JSON codec. Its performance provider is SIMD-accelerated through @aot; compiler and browser targets select a pure-Lua provider behind the same API. It parses, pulls selected fields, serializes, and streams.
local encoded = nupp.codec.json.encode({name = "Nupp", ready = true})
local decoded = assert(nupp.codec.json.decode(encoded))
assert(decoded.name == "Nupp")The codec accepts one complete UTF-8 JSON document and rejects invalid numbers, sparse arrays, mixed-key containers, cycles, and excessive nesting. An object that names one member twice is refused at the second occurrence, as I-JSON requires, even when the first held a null that decoding drops. A number too large for binary64, such as 1e400, is refused with its byte position rather than read as an infinity the encoder would refuse to write back; one too small, such as 1e-400, is zero.
Empty containers#
A plain empty Lua table has no shape to read, so it encodes as {}. Mark it when it must encode as an array, or use the two sentinels:
const json = nupp.codec.json
assert(json.encode({}) == "{}")
assert(json.encode(json.asArray({})) == "[]")
assert(json.encode(json.EMPTY_ARRAY) == "[]")
assert(json.encode(json.EMPTY_OBJECT) == "{}")
local emptyArray = assert(json.decode("[]"))
assert(json.isArray(emptyArray))
assert(not json.isArray(assert(json.decode("{}"))))asObject is the corresponding mark for a table that must encode as an object. isArray reads the shape retained by decoded containers, including when they are empty.
JSON null#
Decoding drops JSON null by default, including from inside an array. Pass a replacement value to preserve it. NULL is the round-trippable choice, because it encodes back as null:
const json = nupp.codec.json
local value = assert(json.decode([[{"items":[1,null,2]}]], json.NULL))
assert(value.items[2] == json.NULL)
assert(json.encode(value) == [[{"items":[1,null,2]}]])Dropping null by default
A Lua table cannot hold nil as a value, so a decoded null has to become either an absence or a sentinel. An absence is what most callers mean, and it keeps a decoded document indexable without every read testing against a sentinel first. A sentinel is what a caller round-tripping somebody else's document means, and that caller says so once, at the decode call, rather than everywhere the value is read.
Pulling selected values#
pull validates the complete document while applying its selection. true selects a complete value, an object shape selects named fields, and arrayOf(shape) applies a shape to every member of an array. Missing and unselected fields are omitted:
const json = nupp.codec.json
local source = '[{"id": 1, "role": "admin", "profile": {"name": "Ada", "age": 36}}]'
local users, reason = json.pull(source, json.arrayOf({id = true, profile = {name = true}}))This is the lower-level path for pull deserializers: it validates the complete input but constructs only the Lua values the shape asks for.
Streaming output#
The nupp.Closeable writer emits checked JSON into caller-owned storage without first building a Lua table or a complete result string. endArray() and endObject() end containers, flush() publishes a bounded staged batch while leaving the document open, and consuming close() requires one complete root:
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startObject():key("items"):startArray():write(1):write(2):endArray():endObject()
writer:close()Lexical destruction closes a live writer automatically. The public writer identity is single-use. Closing publishes its final staged parts and leaves that identity closed; untyped stale references therefore cannot affect a later document.
A prepared nupp.serde JSON traversal can write at the current value position. Its recursive walk drains completed fields and list members at the same threshold, so a large root does not need to exist as one staged string.
encoded(value) performs ordinary encoding once. verified(text) instead validates an existing immutable JSON string without decoding or re-encoding it. Both return a value that write appends without another walk or validation. encodedString(value) and verifiedString(text) provide the corresponding string-only form accepted by both key and write; these handles are interned, so repeated schema keys share their encoded representation.
Typed values#
Select a declaration binding and pass it to the typed JSON codec. A binding chooses the model independently of the format.
local serde = require("nupp.serde")
local json = require("nupp.serde.json")
local record User
id: uint32
end
local users = serde.binding(User)
local value = json.decode(users, '{"id":41}')
assert(json.encode(users, value) == '{"id":41}')See Serialization bindings for mappings, documents, and external models.
Submodules
| Module | Description |
|---|---|
nupp.codec.json.spi |
Module contents
Types
| Type | Kind | Description |
|---|---|---|
EncodedString | record | A JSON string whose immutable encoded bytes have already been produced or validated. |
EncodedValue | record | A complete JSON value whose immutable bytes have already been encoded or validated. |
JSONEncodable | interface | Values that can write a complete JSON value through a checked writer. |
Writer | interface | A checked incremental JSON writer, which newWriter returns. |
Functions
| Function | Kind | Description |
|---|---|---|
arrayOf | function | Applies a pull shape to every member of an array. |
asArray | function | Marks a Lua table as a JSON array, including while it is empty. |
asObject | function | Marks a Lua table as a JSON object. |
decode | function | Decodes a complete JSON document. |
encode | function | Serializes one Lua value as JSON. |
encoded | function | Encodes a Lua value once for trusted raw insertion by a Writer. |
encodedString | function | Encodes and interns a string for trusted use as either a key or value. |
isArray | function | Returns whether a value has JSON array shape. |
newWriter | function | Creates an incremental JSON writer over caller-owned storage. |
numberToken | function | Validates and retains one exact JSON number token. |
pull | function | Materializes only the fields selected by an On-Demand pull shape. |
verified | function | Validates existing JSON bytes once without decoding or re-encoding them. |
verifiedString | function | Validates and interns existing bytes containing a complete JSON string. |
Values
| Value | Kind | Description |
|---|---|---|
EMPTY_ARRAY | variable | An explicit empty JSON array. |
EMPTY_OBJECT | variable | An explicit empty JSON object. |
NULL | variable | Creates a schema-driven JSON codec with an immutable wire profile. |
Types#
EncodedStringrecord#
record EncodedStringA JSON string whose immutable encoded bytes have already been produced or validated.
Writer:key accepts this type without validating UTF-8 or escaping it again. Values produced by encodedString and verifiedString are interned.
EncodedValuerecord#
record EncodedValueA complete JSON value whose immutable bytes have already been encoded or validated.
Values come from encoded or verified; the private token prevents checked callers from manufacturing an unverified fast-path value.
JSONEncodableinterface#
interface JSONEncodable ...Values that can write a complete JSON value through a checked writer.
A handwritten record satisfies it with a writeJSON member:
local record Money is nupp.codec.json.JSONEncodable
cents: integer
function writeJSON(self, exclusive out: nupp.codec.json.Writer): nil
out:startObject():key("cents"):write(self.cents):endObject()
end
endMembers
| Name | Kind | Description |
|---|---|---|
writeJSON | method | Writes this value at the writer's current value position. |
Writerinterface#
A checked incremental JSON writer, which newWriter returns.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startObject():key("ok"):write(true):endObject()
writer:close()
assert(out:tostring() == [[{"ok":true}]])Members
| Name | Kind | Description |
|---|---|---|
startArray | method | Starts an array value. |
startObject | method | Starts an object value. |
key | method | Selects the next object member. |
write | method | Appends a complete Lua value. |
null | method | Appends JSON null. |
endArray | method | Ends the current array. |
endObject | method | Ends the current object. |
flush | method | Publishes staged bytes without ending the document. |
abort | method | Discards staged bytes and prevents further writes. |
close | method | Verifies and publishes a complete root, then releases backing state. |
startArraymethod#
Starts an array value.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startArray():write("first"):endArray()
writer:close()
assert(out:tostring() == "[\"first\"]")Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Writer | the writer |
Returns
| Type | Description |
|---|---|
Writer borrows (self) | this writer |
startObjectmethod#
Starts an object value.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startObject():endObject()
writer:close()
assert(out:tostring() == "{}")Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Writer | the writer |
Returns
| Type | Description |
|---|---|
Writer borrows (self) | this writer |
keymethod#
key: function(exclusive self: Writer, name: string): Writer borrows (self)
& function(exclusive self: Writer, name: EncodedString): Writer borrows (self)Selects the next object member.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startObject():key("answer"):write(42):endObject()
writer:close()
assert(out:tostring() == [[{"answer":42}]])Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Writer | the writer |
name | string | the next member name |
Returns
| Type | Description |
|---|---|
Writer borrows (self) | this writer |
writemethod#
write: function(
exclusive self: Writer,
value: string | number | boolean | {any} | {[string]: any}
): Writer borrows (self)
& function(
exclusive self: Writer,
value: EncodedValue | EncodedString
): Writer borrows (self)Appends a complete Lua value.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startArray():write({id = 41}):endArray()
writer:close()
assert(out:tostring() == "[{\"id\":41}]")Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Writer | the writer |
value | string | number | boolean | {any} | {[string]: any} | the next JSON value |
Returns
| Type | Description |
|---|---|
Writer borrows (self) | this writer |
nullmethod#
Appends JSON null.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startArray():null():endArray()
writer:close()
assert(out:tostring() == "[null]")Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Writer | the writer |
Returns
| Type | Description |
|---|---|
Writer borrows (self) | this writer |
endArraymethod#
Ends the current array.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startArray():startObject():endObject():endArray()
writer:close()
assert(out:tostring() == "[{}]")Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Writer | the writer |
Returns
| Type | Description |
|---|---|
Writer borrows (self) | this writer |
endObjectmethod#
Ends the current object.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startObject():key("items"):startArray():endArray():endObject()
writer:close()
assert(out:tostring() == [[{"items":[]}]])Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Writer | the writer |
Returns
| Type | Description |
|---|---|
Writer borrows (self) | this writer |
flushmethod#
flush: function(exclusive self: Writer): nilPublishes staged bytes without ending the document.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startArray():write(1)
writer:flush()
assert(out:tostring() == "[1")
writer:endArray()
writer:close()
assert(out:tostring() == "[1]")Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Writer | the writer |
Returns
| Type | Description |
|---|---|
nil |
abortmethod#
abort: function(exclusive self: Writer): nilDiscards staged bytes and prevents further writes.
Bytes already flushed to the destination remain there. Repeated aborts and closing an aborted writer are harmless.
Arguments
| Name | Type | Description |
|---|---|---|
exclusive self | Writer | the writer |
Returns
| Type | Description |
|---|---|
nil |
closemethod#
Verifies and publishes a complete root, then releases backing state.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:write({ok = true})
writer:close()
assert(out:tostring() == [[{"ok":true}]])Arguments
| Name | Type | Description |
|---|---|---|
takes self | Writer | the writer |
Returns
| Type | Description |
|---|---|
nil |
Functions#
arrayOffunction#
Applies a pull shape to every member of an array.
const json = nupp.codec.json
local users = assert(json.pull(
[[{"users":[{"id":1,"name":"Ada"},{"id":2,"name":"Lin"}]}]],
{users = json.arrayOf({id = true})}
))
assert(users.users[1].id == 1 and users.users[1].name == nil)
assert(users.users[2].id == 2)Arguments
| Name | Type | Description |
|---|---|---|
shape | any? | the selection applied to each member, or every value when omitted |
Returns
| Type | Description |
|---|---|
table | an array pull shape |
asArrayfunction#
Marks a Lua table as a JSON array, including while it is empty.
Type parameters
| Name | Description |
|---|---|
T |
Arguments
| Name | Type | Description |
|---|---|---|
takes value | T | the table to mark |
Returns
| Type | Description |
|---|---|
T preserves value | the same table |
asObjectfunction#
Marks a Lua table as a JSON object.
const json = nupp.codec.json
local object = json.asObject(json.asArray({}))
assert(json.encode(object) == "{}")Type parameters
| Name | Description |
|---|---|
T |
Arguments
| Name | Type | Description |
|---|---|---|
takes value | T | the table to mark |
Returns
| Type | Description |
|---|---|
T preserves value | the same table |
decodefunction#
function decode(text: string, nullValue: any?): any, string?Decodes a complete JSON document.
Null members are dropped unless a replacement value is supplied. Text that is not one JSON document is an ordinary answer: the result is nil and the second one says why. The reason, not the value, is what tells failure apart, since the document null decodes to nil when nulls are dropped.
local value, reason = nupp.codec.json.decode([[{"id":41}]])
assert(reason == nil and value.id == 41)
local broken, why = nupp.codec.json.decode([[{"id":]])
assert(broken == nil and why ~= nil)Arguments
| Name | Type | Description |
|---|---|---|
text | string | the complete JSON document |
nullValue | any? | the value used for JSON null, or nil to drop nulls |
Returns
| Type | Description |
|---|---|
any | the decoded Lua value, or nil when the text is not one document |
string? | why the text is not one JSON document, when it is not |
encodefunction#
function encode(value: any, nullValue: any?): stringSerializes one Lua value as JSON.
Arguments
| Name | Type | Description |
|---|---|---|
value | any | the value to serialize |
nullValue | any? | an additional value treated as JSON null |
Returns
| Type | Description |
|---|---|
string | the JSON document |
encodedfunction#
function encoded(value: any, nullValue: any?): EncodedValueEncodes a Lua value once for trusted raw insertion by a Writer.
The returned handle retains immutable encoded bytes. Each later write copies those bytes directly to its destination without walking or validating the value.
Arguments
| Name | Type | Description |
|---|---|---|
value | any | the value to encode |
nullValue | any? | an additional value treated as JSON null |
Returns
| Type | Description |
|---|---|
EncodedValue | the reusable encoded value |
encodedStringfunction#
function encodedString(value: string): EncodedStringEncodes and interns a string for trusted use as either a key or value.
local key = nupp.codec.json.encodedString("user_id")Arguments
| Name | Type | Description |
|---|---|---|
value | string | the unencoded string |
Returns
| Type | Description |
|---|---|
EncodedString | its reusable encoded representation |
isArrayfunction#
function isArray(value: any): booleanReturns whether a value has JSON array shape.
Decoding retains container shape even when an array is empty. This predicate reads that shape without exposing the private marker used by the codec.
const json = nupp.codec.json
assert(json.isArray(assert(json.decode("[]"))))
assert(not json.isArray(assert(json.decode("{}"))))
assert(json.isArray({1, 2}))Arguments
| Name | Type | Description |
|---|---|---|
value | any | the value whose JSON container shape to inspect |
Returns
| Type | Description |
|---|---|
boolean | true for a JSON array and false for an object or scalar |
Raises
| Type | Condition |
|---|---|
string | if a table mixes object and array keys or contains array holes |
newWriterfunction#
function newWriter(exclusive out: Buffer, nullValue: any?): WriterCreates an incremental JSON writer over caller-owned storage.
local out = nupp.text.newBuffer()
local writer = nupp.codec.json.newWriter(out)
writer:startObject():key("id"):write(41):endObject()
writer:close()
assert(out:tostring() == [[{"id":41}]])Arguments
| Name | Type | Description |
|---|---|---|
exclusive out | Buffer | the destination receiving bytes as operations complete |
nullValue | any? | an additional value treated as JSON null |
Returns
| Type | Description |
|---|---|
Writer | a new writer |
numberTokenfunction#
function numberToken(text: string): EncodedValueValidates and retains one exact JSON number token.
local json = nupp.codec.json
assert(json.encode(json.numberToken("12345678901234567890")) == "12345678901234567890")Arguments
| Name | Type | Description |
|---|---|---|
text | string | the exact numeric lexeme |
Returns
| Type | Description |
|---|---|
EncodedValue | a validated fragment for a JSON writer or encoder |
Raises
| Type | Condition |
|---|---|
string | when text is not one JSON number token |
pullfunction#
Materializes only the fields selected by an On-Demand pull shape.
The whole document is still validated. Text that is not one JSON document, or a value the shape cannot select from, returns nil and a reason, as decode does.
local value = assert(nupp.codec.json.pull(
[[{"id":41,"profile":{"name":"Ada","admin":true}}]],
{id = true, profile = {name = true}}
))
assert(value.id == 41 and value.profile.name == "Ada")
assert(value.profile.admin == nil)Arguments
| Name | Type | Description |
|---|---|---|
text | string | the complete JSON document |
shape | any | the fields and array members to materialize |
nullValue | any? | the value used for JSON null, or nil to drop nulls |
Returns
| Type | Description |
|---|---|
any | the selected Lua value, or nil when the text cannot be pulled |
string? | why the text cannot be pulled, when it cannot |
verifiedfunction#
function verified(text: string): EncodedValueValidates existing JSON bytes once without decoding or re-encoding them.
Arguments
| Name | Type | Description |
|---|---|---|
text | string | a complete JSON value |
Returns
| Type | Description |
|---|---|
EncodedValue | a handle retaining the original immutable string |
verifiedStringfunction#
function verifiedString(text: string): EncodedStringValidates and interns existing bytes containing a complete JSON string.
local key = nupp.codec.json.verifiedString([["user_id"]])Arguments
| Name | Type | Description |
|---|---|---|
text | string | an encoded JSON string, including its quotes |
Returns
| Type | Description |
|---|---|
EncodedString | a handle retaining the original immutable string |
Values#
EMPTY_ARRAYvariable#
const EMPTY_ARRAY: tableAn explicit empty JSON array.
EMPTY_OBJECTvariable#
const EMPTY_OBJECT: tableAn explicit empty JSON object.
NULLvariable#
const NULL: anyCreates a schema-driven JSON codec with an immutable wire profile. Sentinel that decodes from and encodes as JSON null.