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<T> items this appends an empty optional; for std::unique_ptr<T>
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
This commit is contained in:
2026-06-18 10:18:11 -04:00
parent db759a9333
commit 47f1077100
3 changed files with 64 additions and 1 deletions
+18
View File
@@ -71,6 +71,24 @@
}, },
"tree": { "tree": {
"$ref": "#/$defs/Node" "$ref": "#/$defs/Node"
},
"nullable_hobbies": {
"type": "array",
"items": {
"type": [
"string",
"null"
]
}
},
"nullable_scores": {
"type": "array",
"items": {
"type": [
"integer",
"null"
]
}
} }
}, },
"$defs": { "$defs": {
+13 -1
View File
@@ -86,7 +86,9 @@ int main() {
"value": 1, "value": 1,
"children": [ { "value": 2 }, { "value": 3 } ], "children": [ { "value": 2 }, { "value": 3 } ],
"next": { "value": 99 } "next": { "value": 99 }
} },
"nullable_hobbies": [null, "math", null, "lace", null],
"nullable_scores": [null, 10, null, 20, null]
})"; })";
RootBuilder b; RootBuilder b;
WeaselJsonStatus s = parseStrided(b, json); 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)[0].value == 2);
CHECK(r.tree && r.tree->children && (*r.tree->children)[1].value == 3); 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.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"); printf("ok happy path (byte-strided)\n");
} }
} }
+33
View File
@@ -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): def _is_strict(self):
strict = [n for n, o in self.b.objects.items() if o.strict] strict = [n for n, o in self.b.objects.items() if o.strict]
if not strict: if not strict:
@@ -964,6 +992,10 @@ private:
valueComplete(); valueComplete();
}} }}
{self._is_array_kind()}
{self._append_null()}
void cbNull() {{ void cbNull() {{
if (error_) return; if (error_) return;
Frame &f = stack_.back(); Frame &f = stack_.back();
@@ -971,6 +1003,7 @@ private:
SlotInfo si = slotInfoG(f); SlotInfo si = slotInfoG(f);
if (si.cat == Cat::Skip) {{ valueComplete(); return; }} if (si.cat == Cat::Skip) {{ valueComplete(); return; }}
if (!si.nullable) {{ reject(); return; }} if (!si.nullable) {{ reject(); return; }}
if (isArrayKind(f.kind)) appendNull(f); // keep null array elements
valueComplete(); // leave optional empty / pointer null valueComplete(); // leave optional empty / pointer null
}} }}