~bigbes/tarantool

tarantool-protobuf

ref: 7ae72311d3e4722ba8a95acb95c60c0496b6cb75 tarantool-protobuf/docs/howto/04-wkt-struct-value.md -rw-r--r-- 4.9 KiB
7ae72311 — Eugene Blikh docs(readme): vendoring an upstream .proto schema 3 months ago

#How-to: round-tripping Struct, Value, and ListValue

google.protobuf.Struct, Value, and ListValue are designed to carry arbitrary JSON-shaped data through proto. They're the WKT slice with the most foot-guns — null sentinel, disambiguation between Struct vs Value vs ListValue, integer-vs-float — so this how-to walks through the Lua-side conventions.

#The shapes

Proto type Lua-side value Use when
Struct plain Lua table with string keys The field is typed Struct.
ListValue plain Lua array (1-based contiguous) The field is typed ListValue.
Value native Lua of matching shape (scalar / table / array / pb.NULL) The field is Value — the codec auto-detects via Lua type.

Pick Struct or ListValue whenever you can — they're unambiguous. Pick Value when the schema needs a "this could be anything" cell.

#Setting a Value field

local pb = require('pb')
local hello = require('full.hello.hello_pb')  -- has Event.attribute: Value

-- Scalars round-trip natively:
hello.Event_encode({attribute = 'a string'})
hello.Event_encode({attribute = 42})
hello.Event_encode({attribute = true})
hello.Event_encode({attribute = pb.NULL})      -- JSON null

For container shapes, the codec needs to know whether you mean a Struct (string-keyed map) or a ListValue (1-based array). Two ways to disambiguate:

-- 1. Tag with pb.wkt.struct(t) / pb.wkt.list(t):
hello.Event_encode({attribute = pb.wkt.struct({nested = 'x'})})
hello.Event_encode({attribute = pb.wkt.list({1, 2, 3})})

-- 2. Or pass a table the codec can auto-classify: dictionary-shaped
--    tables become Struct, 1-based contiguous arrays become ListValue.
hello.Event_encode({attribute = {nested = 'x'}})  -- Struct
hello.Event_encode({attribute = {1, 2, 3}})       -- ListValue

The auto-classifier is good enough for most cases. Use the tags when the table is ambiguous (empty {}, mixed keys, etc.) — explicit beats the heuristic.

#Setting a Struct field (typed)

When the schema is google.protobuf.Struct directly, the field always holds a string-keyed table — no disambiguation needed:

hello.Event_encode({
    payload = {
        greeting = 'hi',
        count = 42,
        active = true,
        when = pb.NULL,
        nested = {a = 1, b = 2},
    },
})

Decoded:

local d = hello.Event_decode(bytes)
print(d.payload.greeting)             -- 'hi'
print(d.payload.count)                -- 42
print(d.payload.when == pb.NULL)      -- true
print(d.payload.nested.a)             -- 1

#Null vs absent

Two distinct concepts:

  • Absent — the field wasn't set. t.foo is nil (for Value / Struct) or {} (proto3 default for repeated/map).
  • Null — the field is explicitly Value{null_value} / Struct value of null. Lua-side sentinel is pb.NULL.
hello.Event_encode({payload = {seen = pb.NULL}})        -- explicit null
hello.Event_encode({payload = {}})                      -- empty Struct
hello.Event_encode({})                                  -- payload absent

pb.NULL is equal to box.NULL. Use pb.NULL so app code doesn't need to require('box') just for the sentinel.

#Number gotchas

Proto3 Value is a double under the hood — integers and floats share the wire encoding. The codec preserves Lua integer-vs-float when encoding (42 stays integer; 42.5 stays float), but on the decode side everything that travels through Value comes back as a Lua number. If you need a 64-bit integer through Value you must either:

  1. Box it as a Struct with two fields (hi / lo), or
  2. String-encode it, or
  3. Switch to a strongly-typed schema field (int64).

This is a Value limitation, not a codec one — JSON has the same trade-off and Value is Struct-shaped for JSON-compat reasons.

#Mixed lists

ListValue carries an array of Values, so mixed-type arrays work:

hello.Event_encode({tags = {'a', 'b', 3, true, pb.NULL}})

local d = hello.Event_decode(bytes)
-- d.tags = {'a', 'b', 3, true, pb.NULL}

Each entry passes through the same Value auto-classification as above, so tables inside a ListValue need the same pb.wkt.struct / pb.wkt.list tagging when ambiguous.

#Round-tripping through JSON

The proto3 JSON mapping for Struct / Value / ListValue is "native JSON" — no envelope, just the shape itself. pb.json.encode emits the table content directly:

local pbjson = require('pb').json
print(pbjson.encode(hello.Event_descriptor, {
    payload = {greeting = 'hi'},
}))
-- {"payload":{"greeting":"hi"}}

This matches mainline protoc and is what makes Struct useful as a JSON-in-proto cell.

#What's next