# `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.