# `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.
```nupp:playground
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:
```nupp
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:
```nupp
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]}]])
```
::: deepdive 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:
```nupp
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:
```nupp
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.
::: seealso
- [Reflection](../../../../learn/language/reflection/index.html#json-through-a-type-witness) for
the derived schema and the generated `writeJSON` and `fromJSON` members
- [nupp.derive](../../../../modules/nupp/derive/index.html#json) for what `nupp.derive.JSON` adds
to a declaration
- `nupp.serde` for binary serialization
:::
## Constructors
### `newCodec` _constructor_
```nupp
function newCodec(options: any?): any
```
Creates a schema-driven JSON codec with one immutable wire profile.
The codec prepares a `nupp.serde.Binding` once and then performs each
complete scalar structure traversal in one native call. Format-owning JSON derives
remain available as compatibility APIs.
```nupp
@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
| Name | Type | Description |
| --- | --- | --- |
| `options` | `any?` | fieldNames and unknownMembers profile policy |
#### Returns
| Type | Description |
| --- | --- |
| `any` | the schema-driven codec |
#### Raises
- when the profile is invalid
## Types
### `EncodedString` _record_
```nupp
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.
### `EncodedValue` _record_
```nupp
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.
### `JSONEncodable` _interface_
```nupp
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()`.
```nupp
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`
```nupp
writeJSON: function(self, exclusive out: Writer): nil
```
Writes this value at the writer's current value position.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
| `exclusive out` | `Writer` | the checked destination |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
### `Writer` _interface_
```nupp
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.
```nupp
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`
```nupp
startArray: function(exclusive self: Writer): Writer borrows (self)
```
Starts an array value.
```nupp
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
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Writer` | the writer |
###### Returns
| Type | Description |
| --- | --- |
| `Writer borrows (self)` | this writer |
##### `startObject`
```nupp
startObject: function(exclusive self: Writer): Writer borrows (self)
```
Starts an object value.
```nupp
local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(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 |
##### `key`
```nupp
key: function(exclusive self: Writer, name: string): Writer borrows (self)
& function(exclusive self: Writer, name: EncodedString): Writer borrows (self)
```
Selects the next object member.
```nupp
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
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Writer` | the writer |
| `name` | `string` | the next member name |
###### Returns
| Type | Description |
| --- | --- |
| `Writer borrows (self)` | this writer |
##### `write`
```nupp
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.
```nupp
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
| 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 |
##### `null`
```nupp
null: function(exclusive self: Writer): Writer borrows (self)
```
Appends JSON null.
```nupp
local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(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 |
##### `endArray`
```nupp
endArray: function(exclusive self: Writer): Writer borrows (self)
```
Ends the current array.
```nupp
local out = nupp.text.buffer.new()
local writer = nupp.codec.json.writer(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 |
##### `endObject`
```nupp
endObject: function(exclusive self: Writer): Writer borrows (self)
```
Ends the current object.
```nupp
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
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Writer` | the writer |
###### Returns
| Type | Description |
| --- | --- |
| `Writer borrows (self)` | this writer |
##### `flush`
```nupp
flush: nosuspend function(exclusive self: Writer): nil
```
Publishes staged bytes without ending the document.
```nupp
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
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Writer` | the writer |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `close`
```nupp
close: nosuspend function(takes self: Writer): nil
```
Verifies and publishes one complete root, then releases backing state.
```nupp
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
| Name | Type | Description |
| --- | --- | --- |
| `takes self` | `Writer` | the writer |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
## Functions
### `arrayOf` _function_
```nupp
function arrayOf(shape: any?): table
```
Applies one pull shape to every member of an array.
```nupp
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
| Name | Type | Description |
| --- | --- | --- |
| `shape` | `any?` | the selection applied to each member, or every value when omitted |
#### Returns
| Type | Description |
| --- | --- |
| `table` | an array pull shape |
### `asArray` _function_
```nupp
function asArray(takes value: T): T preserves value
```
Marks a Lua table as a JSON array, including while it is empty.
```nupp
local values = nupp.codec.json.asArray({})
assert(nupp.codec.json.encode(values) == "[]")
```
#### 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 |
### `asObject` _function_
```nupp
function asObject(takes value: T): T preserves value
```
Marks a Lua table as a JSON object.
```nupp
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 |
### `decode` _function_
```nupp
function decode(text: string, nullValue: any?): any
```
Decodes one complete JSON document.
Null members are dropped unless a replacement value is supplied.
```nupp
local value = nupp.codec.json.decode([[{"id":41}]])
assert(value.id == 41)
```
#### 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 |
### `decodeAs` _function_
```nupp
function decodeAs(type: Type, text: string): T?, string?
```
Decodes JSON into the nominal record named by a type witness.
```nupp
@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
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `type` | `Type\` | the record type to decode |
| `text` | `string` | the JSON document |
#### Returns
| Type | Description |
| --- | --- |
| `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
### `encode` _function_
```nupp
function encode(value: any, nullValue: any?): string
```
Serializes one Lua value as JSON.
```nupp
local text = nupp.codec.json.encode({answer = 42})
assert(text == [[{"answer":42}]])
```
#### 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 |
### `encodeAs` _function_
```nupp
function encodeAs(type: Type, borrows value: T): string
```
Encodes a nominal record named by a type witness into a new string.
```nupp
@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
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `type` | `Type\` | the record type being encoded |
| `borrows value` | `T` | the record value |
#### Returns
| Type | Description |
| --- | --- |
| `string` | the JSON document |
#### Raises
- when JSON was not derived for the type
### `encoded` _function_
```nupp
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.
```nupp
local cached = nupp.codec.json.encoded({1, 2, 3})
```
#### 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 |
### `encodedString` _function_
```nupp
function encodedString(value: string): EncodedString
```
Encodes and interns one string for trusted use as either a key or value.
```nupp
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 |
### `encodeRecord` _function_
```nupp
function encodeRecord(borrows value: T): string
```
Encodes a nominal record using its own metatable into a new string.
```nupp
@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
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows value` | `T` | the record value |
#### Returns
| Type | Description |
| --- | --- |
| `string` | the JSON document |
#### Raises
- when JSON was not derived for the type
### `isArray` _function_
```nupp
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.
```nupp
const json = nupp.codec.json
assert(json.isArray(json.decode("[]")))
assert(not json.isArray(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
- if a table mixes object and array keys or contains array holes
### `pull` _function_
```nupp
function pull(text: string, shape: any, nullValue: any?): any
```
Materializes only the fields selected by an On-Demand pull shape.
```nupp
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
| 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 |
### `serialize` _function_
```nupp
function serialize(value: any, nullValue: any?): string
```
Serializes one Lua value as JSON; an alias of `encode`.
```nupp
assert(nupp.codec.json.serialize({ok = true}) == [[{"ok":true}]])
```
#### 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 |
### `verified` _function_
```nupp
function verified(text: string): EncodedValue
```
Validates existing JSON bytes once without decoding or re-encoding them.
```nupp
local cached = nupp.codec.json.verified([[{"id":41}]])
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `text` | `string` | one complete JSON value |
#### Returns
| Type | Description |
| --- | --- |
| `EncodedValue` | a handle retaining the original immutable string |
### `verifiedString` _function_
```nupp
function verifiedString(text: string): EncodedString
```
Validates and interns existing bytes containing one complete JSON string.
```nupp
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 |
### `writeAs` _function_
```nupp
function writeAs(type: Type, borrows value: T, exclusive out: Writer): nil
```
Writes a nominal record after explicitly supplying its type witness.
```nupp
@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
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `type` | `Type\` | the record type being encoded |
| `borrows value` | `T` | the record value |
| `exclusive out` | `Writer` | the checked destination |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- when JSON was not derived for the type
### `writer` _function_
```nupp
function writer(exclusive out: Buffer, nullValue: any?): Writer
```
Creates an incremental JSON writer over caller-owned storage.
```nupp
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
| 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 |
### `writeRecord` _function_
```nupp
function writeRecord(borrows value: T, exclusive out: Writer): nil
```
Writes a nominal record using its own metatable as the type witness.
```nupp
@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
| Name | Description |
| --- | --- |
| `T` | |
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows value` | `T` | the record value |
| `exclusive out` | `Writer` | the checked destination |
#### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Raises
- when JSON was not derived for the type
## Values
### `EMPTY_ARRAY` _variable_
```nupp
const EMPTY_ARRAY: table
```
An explicit empty JSON array.
### `EMPTY_OBJECT` _variable_
```nupp
const EMPTY_OBJECT: table
```
An explicit empty JSON object.
### `NULL` _variable_
```nupp
const NULL: any
```
Sentinel that decodes from and encodes as JSON null.