Commit 781a21af2f

Ethan Gruffudd <78272448+EthanGruffudd@users.noreply.github.com>
2021-01-30 22:19:18
std.json: option to ignore unknown fields
Closes #7906
1 parent 0ad1c04
Changed files (1)
lib
lib/std/json.zig
@@ -1105,6 +1105,10 @@ pub const TokenStream = struct {
         };
     }
 
+    fn stackUsed(self: *TokenStream) u8 {
+        return self.parser.stack_used + if (self.token != null) @as(u8, 1) else 0;
+    }
+
     pub fn next(self: *TokenStream) Error!?Token {
         if (self.token) |token| {
             self.token = null;
@@ -1457,8 +1461,73 @@ pub const ParseOptions = struct {
         Error,
         UseLast,
     } = .Error,
+
+    /// If false, finding an unknown field returns an error.
+    ignore_unknown_fields: bool = false,
 };
 
+fn skipValue(tokens: *TokenStream) !void {
+    const original_depth = tokens.stackUsed();
+
+    // Return an error if no value is found
+    _ = try tokens.next();
+    if (tokens.stackUsed() < original_depth) return error.UnexpectedJsonDepth;
+    if (tokens.stackUsed() == original_depth) return;
+
+    while (try tokens.next()) |_| {
+        if (tokens.stackUsed() == original_depth) return;
+    }
+}
+
+test "skipValue" {
+    try skipValue(&TokenStream.init("false"));
+    try skipValue(&TokenStream.init("true"));
+    try skipValue(&TokenStream.init("null"));
+    try skipValue(&TokenStream.init("42"));
+    try skipValue(&TokenStream.init("42.0"));
+    try skipValue(&TokenStream.init("\"foo\""));
+    try skipValue(&TokenStream.init("[101, 111, 121]"));
+    try skipValue(&TokenStream.init("{}"));
+    try skipValue(&TokenStream.init("{\"foo\": \"bar\"}"));
+
+    { // An absurd number of nestings
+        const nestings = 256;
+
+        testing.expectError(
+            error.TooManyNestedItems,
+            skipValue(&TokenStream.init("[" ** nestings ++ "]" ** nestings)),
+        );
+    }
+
+    { // Would a number token cause problems in a deeply-nested array?
+        const nestings = 255;
+        const deeply_nested_array = "[" ** nestings ++ "0.118, 999, 881.99, 911.9, 725, 3" ++ "]" ** nestings;
+
+        try skipValue(&TokenStream.init(deeply_nested_array));
+
+        testing.expectError(
+            error.TooManyNestedItems,
+            skipValue(&TokenStream.init("[" ++ deeply_nested_array ++ "]")),
+        );
+    }
+
+    // Mismatched brace/square bracket
+    testing.expectError(
+        error.UnexpectedClosingBrace,
+        skipValue(&TokenStream.init("[102, 111, 111}")),
+    );
+
+    { // should fail if no value found (e.g. immediate close of object)
+        var empty_object = TokenStream.init("{}");
+        assert(.ObjectBegin == (try empty_object.next()).?);
+        testing.expectError(error.UnexpectedJsonDepth, skipValue(&empty_object));
+
+        var empty_array = TokenStream.init("[]");
+        assert(.ArrayBegin == (try empty_array.next()).?);
+        testing.expectError(error.UnexpectedJsonDepth, skipValue(&empty_array));
+    }
+}
+
 fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options: ParseOptions) !T {
     switch (@typeInfo(T)) {
         .Bool => {
@@ -1598,7 +1667,14 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:
                                 break;
                             }
                         }
-                        if (!found) return error.UnknownField;
+                        if (!found) {
+                            if (options.ignore_unknown_fields) {
+                                try skipValue(tokens);
+                                continue;
+                            } else {
+                                return error.UnknownField;
+                            }
+                        }
                     },
                     else => return error.UnexpectedToken,
                 }
@@ -2040,6 +2116,46 @@ test "parse into struct with duplicate field" {
     try testing.expectError(error.UnexpectedValue, parse(T3, &TokenStream.init(str), options_last));
 }
 
+test "parse into struct ignoring unknown fields" {
+    const T = struct {
+        int: i64,
+        language: []const u8,
+    };
+
+    const ops = ParseOptions{
+        .allocator = testing.allocator,
+        .ignore_unknown_fields = true,
+    };
+
+    const r = try parse(T, &std.json.TokenStream.init(
+        \\{
+        \\  "int": 420,
+        \\  "float": 3.14,
+        \\  "with\\escape": true,
+        \\  "with\u0105unicode\ud83d\ude02": false,
+        \\  "optional": null,
+        \\  "static_array": [66.6, 420.420, 69.69],
+        \\  "dynamic_array": [66.6, 420.420, 69.69],
+        \\  "complex": {
+        \\    "nested": "zig"
+        \\  },
+        \\  "veryComplex": [
+        \\    {
+        \\      "foo": "zig"
+        \\    }, {
+        \\      "foo": "rocks"
+        \\    }
+        \\  ],
+        \\  "a_union": 100000,
+        \\  "language": "zig"
+        \\}
+    ), ops);
+    defer parseFree(T, r, ops);
+
+    testing.expectEqual(@as(i64, 420), r.int);
+    testing.expectEqualSlices(u8, "zig", r.language);
+}
+
 /// A non-stream JSON parser which constructs a tree of Value's.
 pub const Parser = struct {
     allocator: *Allocator,