From 47f1077100136ae2abaca5a8d263efc1fda0702d Mon Sep 17 00:00:00 2001 From: Weaselbot Date: Thu, 18 Jun 2026 10:18:11 -0400 Subject: [PATCH] schemagen: keep null elements in arrays with nullable item types For array types whose items are nullable ({"type": ["T", "null"]}), the generated builder previously called valueComplete() on cbNull() without appending anything to the owning vector. Null entries were silently dropped, so vector indices no longer matched JSON array indices. Generate isArrayKind() / appendNull() helpers and have cbNull() append a default-constructed element when the current frame is an array. For std::optional items this appends an empty optional; for std::unique_ptr items it appends a null pointer. Add nullable string/integer array fields to the example schema and test coverage to verify indices are preserved. Fixes #5 --- contrib/schemagen/example.schema.json | 18 +++++++++++++ contrib/schemagen/test_gen.cpp | 14 +++++++++- contrib/schemagen/weaseljson_schemagen.py | 33 +++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/contrib/schemagen/example.schema.json b/contrib/schemagen/example.schema.json index 1bd6180..e1c0f6b 100644 --- a/contrib/schemagen/example.schema.json +++ b/contrib/schemagen/example.schema.json @@ -71,6 +71,24 @@ }, "tree": { "$ref": "#/$defs/Node" + }, + "nullable_hobbies": { + "type": "array", + "items": { + "type": [ + "string", + "null" + ] + } + }, + "nullable_scores": { + "type": "array", + "items": { + "type": [ + "integer", + "null" + ] + } } }, "$defs": { diff --git a/contrib/schemagen/test_gen.cpp b/contrib/schemagen/test_gen.cpp index b507b93..577e983 100644 --- a/contrib/schemagen/test_gen.cpp +++ b/contrib/schemagen/test_gen.cpp @@ -86,7 +86,9 @@ int main() { "value": 1, "children": [ { "value": 2 }, { "value": 3 } ], "next": { "value": 99 } - } + }, + "nullable_hobbies": [null, "math", null, "lace", null], + "nullable_scores": [null, 10, null, 20, null] })"; RootBuilder b; WeaselJsonStatus s = parseStrided(b, json); @@ -116,6 +118,16 @@ int main() { CHECK(r.tree && r.tree->children && (*r.tree->children)[0].value == 2); CHECK(r.tree && r.tree->children && (*r.tree->children)[1].value == 3); CHECK(r.tree && r.tree->next && r.tree->next->value == 99); + CHECK(r.nullable_hobbies.has_value() && r.nullable_hobbies->size() == 5); + CHECK(r.nullable_hobbies && !(*r.nullable_hobbies)[0] && + (*r.nullable_hobbies)[1] && *(*r.nullable_hobbies)[1] == "math" && + !(*r.nullable_hobbies)[2] && (*r.nullable_hobbies)[3] && + *(*r.nullable_hobbies)[3] == "lace" && !(*r.nullable_hobbies)[4]); + CHECK(r.nullable_scores.has_value() && r.nullable_scores->size() == 5); + CHECK(r.nullable_scores && !(*r.nullable_scores)[0] && + (*r.nullable_scores)[1] && *(*r.nullable_scores)[1] == 10 && + !(*r.nullable_scores)[2] && (*r.nullable_scores)[3] && + *(*r.nullable_scores)[3] == 20 && !(*r.nullable_scores)[4]); printf("ok happy path (byte-strided)\n"); } } diff --git a/contrib/schemagen/weaseljson_schemagen.py b/contrib/schemagen/weaseljson_schemagen.py index e7a1a6c..34d6d52 100644 --- a/contrib/schemagen/weaseljson_schemagen.py +++ b/contrib/schemagen/weaseljson_schemagen.py @@ -690,6 +690,34 @@ namespace {ns} {{""" " }" ) + def _is_array_kind(self): + arrs = [n for n, _ in self.arr_types] + if not arrs: + return " bool isArrayKind(Kind) const { return false; }" + cases = " ".join(f"case Kind::{n}:" for n in arrs) + return ( + " bool isArrayKind(Kind k) const {\n" + f" switch (k) {{ {cases} return true; default: return false; }}\n" + " }" + ) + + def _append_null(self): + lines = [ + " void appendNull(Frame &f) {", + " switch (f.kind) {", + ] + for name, tarr in self.arr_types: + vectype = self.base_cpp(tarr) + lines.append(f" case Kind::{name}: {{") + lines.append(f" auto *v = ({vectype} *)f.dest;") + lines.append(" v->emplace_back();") + lines.append(" return;") + lines.append(" }") + lines.append(" default: return;") + lines.append(" }") + lines.append(" }") + return "\n".join(lines) + def _is_strict(self): strict = [n for n, o in self.b.objects.items() if o.strict] if not strict: @@ -964,6 +992,10 @@ private: valueComplete(); }} +{self._is_array_kind()} + +{self._append_null()} + void cbNull() {{ if (error_) return; Frame &f = stack_.back(); @@ -971,6 +1003,7 @@ private: SlotInfo si = slotInfoG(f); if (si.cat == Cat::Skip) {{ valueComplete(); return; }} if (!si.nullable) {{ reject(); return; }} + if (isArrayKind(f.kind)) appendNull(f); // keep null array elements valueComplete(); // leave optional empty / pointer null }}