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

ModuleDescription
nupp.codec.json.spi

Module contents

Types

TypeKindDescription
EncodedStringrecordA JSON string whose immutable encoded bytes have already been produced or validated.
EncodedValuerecordA complete JSON value whose immutable bytes have already been encoded or validated.
JSONEncodableinterfaceValues that can write a complete JSON value through a checked writer.
WriterinterfaceA checked incremental JSON writer, which newWriter returns.

Functions

FunctionKindDescription
arrayOffunctionApplies a pull shape to every member of an array.
asArrayfunctionMarks a Lua table as a JSON array, including while it is empty.
asObjectfunctionMarks a Lua table as a JSON object.
decodefunctionDecodes a complete JSON document.
encodefunctionSerializes one Lua value as JSON.
encodedfunctionEncodes a Lua value once for trusted raw insertion by a Writer.
encodedStringfunctionEncodes and interns a string for trusted use as either a key or value.
isArrayfunctionReturns whether a value has JSON array shape.
newWriterfunctionCreates an incremental JSON writer over caller-owned storage.
numberTokenfunctionValidates and retains one exact JSON number token.
pullfunctionMaterializes only the fields selected by an On-Demand pull shape.
verifiedfunctionValidates existing JSON bytes once without decoding or re-encoding them.
verifiedStringfunctionValidates and interns existing bytes containing a complete JSON string.

Values

ValueKindDescription
EMPTY_ARRAYvariableAn explicit empty JSON array.
EMPTY_OBJECTvariableAn explicit empty JSON object.
NULLvariableCreates a schema-driven JSON codec with an immutable wire profile.

Types#

EncodedStringrecord#

record EncodedString

A 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 EncodedValue

A 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
end

Members

NameKindDescription
writeJSONmethodWrites this value at the writer's current value position.

writeJSONmethod#

writeJSON: function(self, exclusive out: Writer): nil

Writes this value at the writer's current value position.

Arguments
NameTypeDescription
?self
exclusive outWriter

the checked destination

Returns
TypeDescription
nil

Writerinterface#

interface Writer is nupp.Closeable ...

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

NameKindDescription
startArraymethodStarts an array value.
startObjectmethodStarts an object value.
keymethodSelects the next object member.
writemethodAppends a complete Lua value.
nullmethodAppends JSON null.
endArraymethodEnds the current array.
endObjectmethodEnds the current object.
flushmethodPublishes staged bytes without ending the document.
abortmethodDiscards staged bytes and prevents further writes.
closemethodVerifies and publishes a complete root, then releases backing state.

startArraymethod#

startArray: function(exclusive self: Writer): Writer borrows (self)

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
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
Writer borrows (self)

this writer

startObjectmethod#

startObject: function(exclusive self: Writer): Writer borrows (self)

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
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
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
NameTypeDescription
exclusive selfWriter

the writer

namestring

the next member name

Returns
TypeDescription
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
NameTypeDescription
exclusive selfWriter

the writer

valuestring | number | boolean | {any} | {[string]: any}

the next JSON value

Returns
TypeDescription
Writer borrows (self)

this writer

nullmethod#

null: function(exclusive self: Writer): Writer borrows (self)

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
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
Writer borrows (self)

this writer

endArraymethod#

endArray: function(exclusive self: Writer): Writer borrows (self)

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
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
Writer borrows (self)

this writer

endObjectmethod#

endObject: function(exclusive self: Writer): Writer borrows (self)

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
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
Writer borrows (self)

this writer

flushmethod#

flush: @nosuspend function(exclusive self: Writer): nil

Publishes 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
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
nil

abortmethod#

abort: @nosuspend function(exclusive self: Writer): nil

Discards staged bytes and prevents further writes.

Bytes already flushed to the destination remain there. Repeated aborts and closing an aborted writer are harmless.

Arguments
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
nil

closemethod#

close: @nosuspend function(takes self: Writer): nil

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
NameTypeDescription
takes selfWriter

the writer

Returns
TypeDescription
nil

Functions#

arrayOffunction#

function arrayOf(shape: any?): table

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

NameTypeDescription
shapeany?

the selection applied to each member, or every value when omitted

Returns

TypeDescription
table

an array pull shape

asArrayfunction#

function asArray<T is table>(takes value: T): T preserves value

Marks a Lua table as a JSON array, including while it is empty.

local values = nupp.codec.json.asArray({})
assert(nupp.codec.json.encode(values) == "[]")

Type parameters

NameDescription
T

Arguments

NameTypeDescription
takes valueT

the table to mark

Returns

TypeDescription
T preserves value

the same table

asObjectfunction#

function asObject<T is table>(takes value: T): T preserves value

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

NameDescription
T

Arguments

NameTypeDescription
takes valueT

the table to mark

Returns

TypeDescription
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

NameTypeDescription
textstring

the complete JSON document

nullValueany?

the value used for JSON null, or nil to drop nulls

Returns

TypeDescription
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?): string

Serializes one Lua value as JSON.

local text = nupp.codec.json.encode({answer = 42})
assert(text == [[{"answer":42}]])

Arguments

NameTypeDescription
valueany

the value to serialize

nullValueany?

an additional value treated as JSON null

Returns

TypeDescription
string

the JSON document

encodedfunction#

function encoded(value: any, nullValue: any?): EncodedValue

Encodes 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.

local cached = nupp.codec.json.encoded({1, 2, 3})

Arguments

NameTypeDescription
valueany

the value to encode

nullValueany?

an additional value treated as JSON null

Returns

TypeDescription
EncodedValue

the reusable encoded value

encodedStringfunction#

function encodedString(value: string): EncodedString

Encodes and interns a string for trusted use as either a key or value.

local key = nupp.codec.json.encodedString("user_id")

Arguments

NameTypeDescription
valuestring

the unencoded string

Returns

TypeDescription
EncodedString

its reusable encoded representation

isArrayfunction#

function isArray(value: any): boolean

Returns 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

NameTypeDescription
valueany

the value whose JSON container shape to inspect

Returns

TypeDescription
boolean

true for a JSON array and false for an object or scalar

Raises

TypeCondition
string

if a table mixes object and array keys or contains array holes

newWriterfunction#

function newWriter(exclusive out: Buffer, nullValue: any?): Writer

Creates 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

NameTypeDescription
exclusive outBuffer

the destination receiving bytes as operations complete

nullValueany?

an additional value treated as JSON null

Returns

TypeDescription
Writer

a new writer

numberTokenfunction#

function numberToken(text: string): EncodedValue

Validates and retains one exact JSON number token.

local json = nupp.codec.json
assert(json.encode(json.numberToken("12345678901234567890")) == "12345678901234567890")

Arguments

NameTypeDescription
textstring

the exact numeric lexeme

Returns

TypeDescription
EncodedValue

a validated fragment for a JSON writer or encoder

Raises

TypeCondition
string

when text is not one JSON number token

pullfunction#

function pull(text: string, shape: any, nullValue: any?): any, string?

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

NameTypeDescription
textstring

the complete JSON document

shapeany

the fields and array members to materialize

nullValueany?

the value used for JSON null, or nil to drop nulls

Returns

TypeDescription
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): EncodedValue

Validates existing JSON bytes once without decoding or re-encoding them.

local cached = nupp.codec.json.verified([[{"id":41}]])

Arguments

NameTypeDescription
textstring

a complete JSON value

Returns

TypeDescription
EncodedValue

a handle retaining the original immutable string

verifiedStringfunction#

function verifiedString(text: string): EncodedString

Validates and interns existing bytes containing a complete JSON string.

local key = nupp.codec.json.verifiedString([["user_id"]])

Arguments

NameTypeDescription
textstring

an encoded JSON string, including its quotes

Returns

TypeDescription
EncodedString

a handle retaining the original immutable string

Values#

EMPTY_ARRAYvariable#

An explicit empty JSON array.

EMPTY_OBJECTvariable#

An explicit empty JSON object.

NULLvariable#

const NULL: any

Creates a schema-driven JSON codec with an immutable wire profile. Sentinel that decodes from and encodes as JSON null.