nupp.codec.json

nupp.codec.json is a strict Nupp JSON codec. Its performance provider is SIMD-accelerated through @aot; portable 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 = 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. serialize is an alias of encode.

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 = json.decode("[]")
assert(json.isArray(emptyArray))
assert(not json.isArray(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 = 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, false drops it, 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 users = 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.buffer.new()
local writer = nupp.codec.json.writer(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.

Derived records#

For a record deriving nupp.derive.JSON, writeRecord(value, writer) discovers the record's type witness from the value. writeAs(Record, value, writer) and decodeAs(Record, text) accept the visible record name directly. encodeRecord and encodeAs allocate and return a complete string for callers that specifically need one.

The generated writeJSON(writer) member writes through the same checked API, so a derived record can occupy the root or any value position in a larger document. Derived schemas lazily cache their encoded field names and literal values.

These five members are installed onto the module by the derive runtime rather than answered by the codec, so each raises when nothing derived JSON for the type it was handed.

Module contents

Constructors

ConstructorDescription
newCodecCreates a schema-driven JSON codec with one immutable wire profile.

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 one complete JSON value through a checked writer.
WriterinterfaceA checked incremental JSON writer.

Functions

FunctionKindDescription
arrayOffunctionApplies one 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 one complete JSON document.
decodeAsfunctionDecodes JSON into the nominal record named by a type witness.
encodefunctionSerializes one Lua value as JSON.
encodeAsfunctionEncodes a nominal record named by a type witness into a new string.
encodedfunctionEncodes a Lua value once for trusted raw insertion by a Writer.
encodedStringfunctionEncodes and interns one string for trusted use as either a key or value.
encodeRecordfunctionEncodes a nominal record using its own metatable into a new string.
isArrayfunctionAnswers whether a value has JSON array shape.
pullfunctionMaterializes only the fields selected by an On-Demand pull shape.
serializefunctionSerializes one Lua value as JSON; an alias of encode.
verifiedfunctionValidates existing JSON bytes once without decoding or re-encoding them.
verifiedStringfunctionValidates and interns existing bytes containing one complete JSON string.
writeAsfunctionWrites a nominal record after explicitly supplying its type witness.
writerfunctionCreates an incremental JSON writer over caller-owned storage.
writeRecordfunctionWrites a nominal record using its own metatable as the type witness.

Values

ValueKindDescription
EMPTY_ARRAYvariableAn explicit empty JSON array.
EMPTY_OBJECTvariableAn explicit empty JSON object.
NULLvariableSentinel that decodes from and encodes as JSON null.

Constructors#

newCodecconstructor#

function newCodec(options: any?): any

Creates a schema-driven JSON codec with one immutable wire profile.

The codec prepares a nupp.serde.Binding<T> once and then performs each complete scalar structure traversal in one native call. Format-owning JSON derives remain available as compatibility APIs.

@derive(nupp.derive.Serde)
local record User
    id: uint32
end

local codec = nupp.codec.json.newCodec()
local prepared = codec:prepare(nupp.serde.of(User))
assert(prepared:encode(new User(id = 41)) == [[{"id":41}]])

Arguments

NameTypeDescription
optionsany?

fieldNames and unknownMembers profile policy

Returns

TypeDescription
any

the schema-driven codec

Raises

  • when the profile is invalid

Types#

EncodedStringrecord#

record EncodedString
end

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
end

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
    writeJSON: function(self, exclusive out: Writer): nil
end

Values that can write one complete JSON value through a checked writer.

Declared here rather than aliased out of the host boundary, because this is the name callers write: derive.JSON answers a contract in terms of it. Writer below names the incremental encoder returned by writer().

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

Methods

writeJSON#
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#

affine interface Writer is nupp.Closeable
    startArray: function(exclusive self: Writer): Writer borrows (self)
    startObject: function(exclusive self: Writer): Writer borrows (self)
    key: function(exclusive self: Writer, name: string): Writer borrows (self)
        & function(exclusive self: Writer, name: EncodedString): Writer borrows (self)
    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)
    null: function(exclusive self: Writer): Writer borrows (self)
    endArray: function(exclusive self: Writer): Writer borrows (self)
    endObject: function(exclusive self: Writer): Writer borrows (self)
    flush: nosuspend function(exclusive self: Writer): nil
    terminal close: nosuspend function(takes self: Writer): nil
end

A checked incremental JSON writer.

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(out)
writer:startObject():key("ok"):write(true):endObject()
writer:close()
assert(out:tostring() == [[{"ok":true}]])

Methods

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

Starts an array value.

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(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

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

Starts an object value.

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(out)
writer:startObject():endObject()
writer:close()
assert(out:tostring() == "{}")
Arguments
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
Writer borrows (self)

this writer

key#
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.buffer.new()
local writer = nupp.codec.json.writer(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

write#
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 one complete Lua value.

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(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

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

Appends JSON null.

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(out)
writer:startArray():null():endArray()
writer:close()
assert(out:tostring() == "[null]")
Arguments
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
Writer borrows (self)

this writer

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

Ends the current array.

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(out)
writer:startArray():startObject():endObject():endArray()
writer:close()
assert(out:tostring() == "[{}]")
Arguments
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
Writer borrows (self)

this writer

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

Ends the current object.

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(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

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

Publishes staged bytes without ending the document.

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(out)
writer:startArray():write(1)
writer:flush()
assert(out:tostring() == "[1")
writer:endArray():close()
Arguments
NameTypeDescription
exclusive selfWriter

the writer

Returns
TypeDescription
nil
close#
close: nosuspend function(takes self: Writer): nil

Verifies and publishes one complete root, then releases backing state.

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(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 one pull shape to every member of an array.

const json = nupp.codec.json
local users = 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

Decodes one complete JSON document.

Null members are dropped unless a replacement value is supplied.

local value = nupp.codec.json.decode([[{"id":41}]])
assert(value.id == 41)

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

decodeAsfunction#

function decodeAs<T>(type: Type<T>, text: string): T?, string?

Decodes JSON into the nominal record named by a type witness.

@derive(nupp.derive.JSON)
local record User
    id: integer
end

local user, problem = nupp.codec.json.decodeAs(User, [[{"id":41}]])
assert(problem == nil and user and user.id == 41)

Type parameters

NameDescription
T

Arguments

NameTypeDescription
typeType<T>

the record type to decode

textstring

the JSON document

Returns

TypeDescription
T?

the decoded record, or nil when it does not fit

string?

the failure reason, when unsuccessful

Raises

  • when JSON was not derived for the type

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

encodeAsfunction#

function encodeAs<T>(type: Type<T>, borrows value: T): string

Encodes a nominal record named by a type witness into a new string.

@derive(nupp.derive.JSON)
local record User
    id: integer
end

local text = nupp.codec.json.encodeAs(User, new User(id = 41))
assert(text == [[{"id":41}]])

Type parameters

NameDescription
T

Arguments

NameTypeDescription
typeType<T>

the record type being encoded

borrows valueT

the record value

Returns

TypeDescription
string

the JSON document

Raises

  • when JSON was not derived for the type

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 one 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

encodeRecordfunction#

function encodeRecord<T>(borrows value: T): string

Encodes a nominal record using its own metatable into a new string.

@derive(nupp.derive.JSON)
local record User
    id: integer
end

local text = nupp.codec.json.encodeRecord(new User(id = 41))
assert(text == [[{"id":41}]])

Type parameters

NameDescription
T

Arguments

NameTypeDescription
borrows valueT

the record value

Returns

TypeDescription
string

the JSON document

Raises

  • when JSON was not derived for the type

isArrayfunction#

function isArray(value: any): boolean

Answers 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(json.decode("[]")))
assert(not json.isArray(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

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

pullfunction#

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

Materializes only the fields selected by an On-Demand pull shape.

local value = 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

serializefunction#

function serialize(value: any, nullValue: any?): string

Serializes one Lua value as JSON; an alias of encode.

assert(nupp.codec.json.serialize({ok = true}) == [[{"ok":true}]])

Arguments

NameTypeDescription
valueany

the value to serialize

nullValueany?

an additional value treated as JSON null

Returns

TypeDescription
string

the JSON document

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

one 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 one 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

writeAsfunction#

function writeAs<T>(type: Type<T>, borrows value: T, exclusive out: Writer): nil

Writes a nominal record after explicitly supplying its type witness.

@derive(nupp.derive.JSON)
local record User
    id: integer
end

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(out)
nupp.codec.json.writeAs(User, new User(id = 41), writer)
writer:close()
assert(out:tostring() == [[{"id":41}]])

Type parameters

NameDescription
T

Arguments

NameTypeDescription
typeType<T>

the record type being encoded

borrows valueT

the record value

exclusive outWriter

the checked destination

Returns

TypeDescription
nil

Raises

  • when JSON was not derived for the type

writerfunction#

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

Creates an incremental JSON writer over caller-owned storage.

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(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

writeRecordfunction#

function writeRecord<T>(borrows value: T, exclusive out: Writer): nil

Writes a nominal record using its own metatable as the type witness.

@derive(nupp.derive.JSON)
local record User
    id: integer
end

local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(out)
nupp.codec.json.writeRecord(new User(id = 41), writer)
writer:close()
assert(out:tostring() == [[{"id":41}]])

Type parameters

NameDescription
T

Arguments

NameTypeDescription
borrows valueT

the record value

exclusive outWriter

the checked destination

Returns

TypeDescription
nil

Raises

  • when JSON was not derived for the type

Values#

EMPTY_ARRAYvariable#

const EMPTY_ARRAY: table

An explicit empty JSON array.

EMPTY_OBJECTvariable#

const EMPTY_OBJECT: table

An explicit empty JSON object.

NULLvariable#

const NULL: any

Sentinel that decodes from and encodes as JSON null.