schemagen: reject duplicate unknown keys in non-strict objects

Track unknown keys in a per-object unordered_set so that permissive
objects (additionalProperties absent/true) still reject duplicate keys,
matching the README guarantee.

- Add std::unordered_set<std::string> to Frame.
- Insert unknown keys in cbKeyData and reject duplicates before skipping.
- Add a permissive "loose" subobject to example.schema.json.
- Test single unknown key accepted and duplicate unknown/known keys rejected.
This commit is contained in:
2026-06-23 15:08:48 -04:00
parent e8830e27e9
commit ab95fefb09
3 changed files with 29 additions and 1 deletions
+8
View File
@@ -52,6 +52,14 @@
}
}
},
"loose": {
"type": "object",
"properties": {
"known": {
"type": "string"
}
}
},
"friends": {
"type": "array",
"items": {
+14
View File
@@ -186,6 +186,20 @@ int main() {
expectReject(R"([1,2,3])", "array where object expected (root)");
expectReject(R"({"name":"x","age":1,)", "truncated / invalid json");
// ---- duplicate keys in permissive (additionalProperties allowed) object
// ----
{
RootBuilder b;
WeaselJsonStatus s =
parseStrided(b, R"({"name":"x","age":1,"loose":{"unknown":1}})");
CHECK(s == WeaselJson_OK);
printf("ok single unknown key in non-strict object\n");
}
expectReject(R"({"name":"x","age":1,"loose":{"unknown":1,"unknown":2}})",
"duplicate unknown key in non-strict object");
expectReject(R"({"name":"x","age":1,"loose":{"known":"a","known":"b"}})",
"duplicate known key in non-strict object");
if (failures == 0) {
printf("\nALL TESTS PASSED\n");
return 0;
+7 -1
View File
@@ -566,6 +566,7 @@ class Emitter:
#include <optional>
#include <string>
#include <string_view>
#include <unordered_set>
#include <vector>
#include "weaseljson.h"
@@ -931,6 +932,7 @@ private:
void *dest;
int field = -1; // object: selected field (-1 want key, -2 skip)
std::vector<uint64_t> seen; // populated field bitset (object frames)
std::unordered_set<std::string> unknown; // unknown keys seen in non-strict objects
}};
static constexpr int kWantKey = -1;
static constexpr int kSkip = -2;
@@ -1014,12 +1016,16 @@ private:
scratch_.append(buf, len);
if (!done) return;
int idx = matchKey(f.kind, scratch_);
scratch_.clear();
if (idx < 0) {{
if (isStrict(f.kind)) {{ reject(); return; }}
if (!f.unknown.insert(scratch_).second) {{
reject(); return; // duplicate unknown key
}}
scratch_.clear();
f.field = kSkip;
return;
}}
scratch_.clear();
if (f.seen[idx >> 6] & (1ull << (idx & 63))) {{
reject(); return; // duplicate key
}}