diff --git a/README.md b/README.md index 1304fc6..9f51b5c 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ You can use `useDefaultSerializationIn` to add serializers of a flavor to a spec requireAllFields = true omitOptionalFields = true allowUnknownFields = true + skipNullFields = false ``` ```Nim diff --git a/json_serialization/format.nim b/json_serialization/format.nim index 119b872..cc808da 100644 --- a/json_serialization/format.nim +++ b/json_serialization/format.nim @@ -24,6 +24,7 @@ template flavorUsesAutomaticObjectSerialization*(T: type DefaultFlavor): bool = template flavorOmitsOptionalFields*(T: type DefaultFlavor): bool = false template flavorRequiresAllFields*(T: type DefaultFlavor): bool = false template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false +template flavorSkipNullFields*(T: type DefaultFlavor): bool = false # We create overloads of these traits to force the mixin treatment of the symbols type DummyFlavor* = object @@ -31,13 +32,15 @@ template flavorUsesAutomaticObjectSerialization*(T: type DummyFlavor): bool = tr template flavorOmitsOptionalFields*(T: type DummyFlavor): bool = false template flavorRequiresAllFields*(T: type DummyFlavor): bool = false template flavorAllowsUnknownFields*(T: type DummyFlavor): bool = false +template flavorSkipNullFields*(T: type DummyFlavor): bool = false template createJsonFlavor*(FlavorName: untyped, mimeTypeValue = "application/json", automaticObjectSerialization = false, requireAllFields = true, omitOptionalFields = true, - allowUnknownFields = true) {.dirty.} = + allowUnknownFields = true, + skipNullFields = false) {.dirty.} = type FlavorName* = object template Reader*(T: type FlavorName): type = Reader(Json, FlavorName) @@ -49,3 +52,4 @@ template createJsonFlavor*(FlavorName: untyped, template flavorOmitsOptionalFields*(T: type FlavorName): bool = omitOptionalFields template flavorRequiresAllFields*(T: type FlavorName): bool = requireAllFields template flavorAllowsUnknownFields*(T: type FlavorName): bool = allowUnknownFields + template flavorSkipNullFields*(T: type FlavorName): bool = skipNullFields diff --git a/json_serialization/lexer.nim b/json_serialization/lexer.nim index 20b6854..b3e9123 100644 --- a/json_serialization/lexer.nim +++ b/json_serialization/lexer.nim @@ -580,7 +580,11 @@ proc scanString*[T](lex: var JsonLexer, val: var T, limit: int) proc scanValue*[T](lex: var JsonLexer, val: var T) {.gcsafe, raises: [IOError].} +proc tokKind*(lex: var JsonLexer): JsonValueKind + {.gcsafe, raises: [IOError].} + template parseObjectImpl*(lex: JsonLexer, + skipNullFields: static[bool], actionInitial: untyped, actionClosing: untyped, actionComma: untyped, @@ -645,7 +649,13 @@ template parseObjectImpl*(lex: JsonLexer, error(lex, errColonExpected, actionError) lex.advance - actionValue + when skipNullFields: + if lex.tokKind() == JsonValueKind.Null: + lex.scanNull() + else: + actionValue + else: + actionValue if not lex.ok: actionError else: error(lex, errStringExpected, actionError) @@ -657,7 +667,7 @@ proc scanObject*[T](lex: var JsonLexer, val: var T) when T isnot (string or JsonVoid or JsonObjectType): {.fatal: "`scanObject` only accepts `string` or `JsonVoid` or `JsonObjectType`".} - parseObjectImpl(lex): + parseObjectImpl(lex, false): # initial action when T is string: val.add '{' diff --git a/json_serialization/parser.nim b/json_serialization/parser.nim index dd5c63c..bfbf5b8 100644 --- a/json_serialization/parser.nim +++ b/json_serialization/parser.nim @@ -211,7 +211,7 @@ proc parseNumber*(r: var JsonReader, val: var JsonNumber) r.raiseParserError(errNumberExpected) r.lex.scanNumber(val) r.checkError - + proc toInt*(r: var JsonReader, val: JsonNumber, T: type SomeSignedInt, portable: bool): T {.gcsafe, raises: [JsonReaderError].}= if val.sign == JsonSign.Neg: @@ -291,6 +291,12 @@ proc parseFloat*(r: var JsonReader, T: type SomeFloat): T proc parseAsString*(r: var JsonReader, val: var string) {.gcsafe, raises: [IOError, JsonReaderError].} = + mixin flavorSkipNullFields + type + Reader = typeof r + Flavor = Reader.Flavor + const skipNullFields = flavorSkipNullFields(Flavor) + case r.tokKind of JsonValueKind.String: escapeJson(r.parseString(), val) @@ -298,7 +304,7 @@ proc parseAsString*(r: var JsonReader, val: var string) r.lex.scanNumber(val) r.checkError of JsonValueKind.Object: - parseObjectImpl(r.lex): + parseObjectImpl(r.lex, skipNullFields): # initial action val.add '{' do: # closing action @@ -355,7 +361,7 @@ proc parseValue*(r: var JsonReader, val: var JsonValueRef) {.gcsafe, raises: [IOError, JsonReaderError].} = r.lex.scanValue(val) r.checkError - + template parseArray*(r: var JsonReader; body: untyped) = if r.tokKind != JsonValueKind.Array: r.raiseParserError(errBracketLeExpected) @@ -375,9 +381,15 @@ template parseArray*(r: var JsonReader; idx: untyped; body: untyped) = do: r.raiseParserError() # error action template parseObject*(r: var JsonReader, key: untyped, body: untyped) = + mixin flavorSkipNullFields + type + Reader = typeof r + Flavor = Reader.Flavor + const skipNullFields = flavorSkipNullFields(typeof Flavor) + if r.tokKind != JsonValueKind.Object: r.raiseParserError(errCurlyLeExpected) - parseObjectImpl(r.lex): discard # initial action + parseObjectImpl(r.lex, skipNullFields): discard # initial action do: discard # closing action do: discard # comma action do: # key action @@ -388,9 +400,15 @@ template parseObject*(r: var JsonReader, key: untyped, body: untyped) = r.raiseParserError() template parseObjectCustomKey*(r: var JsonReader, keyAction: untyped, body: untyped) = + mixin flavorSkipNullFields + type + Reader = typeof r + Flavor = Reader.Flavor + const skipNullFields = flavorSkipNullFields(Flavor) + if r.tokKind != JsonValueKind.Object: r.raiseParserError(errCurlyLeExpected) - parseObjectImpl(r.lex): discard # initial action + parseObjectImpl(r.lex, skipNullFields): discard # initial action do: discard # closing action do: discard # comma action do: # key action @@ -414,6 +432,12 @@ proc readJsonNodeField(r: var JsonReader, field: var JsonNode) field = r.parseJsonNode() proc parseJsonNode(r: var JsonReader): JsonNode = + mixin flavorSkipNullFields + type + Reader = typeof r + Flavor = Reader.Flavor + const skipNullFields = flavorSkipNullFields(Flavor) + case r.tokKind of JsonValueKind.String: result = JsonNode(kind: JString, str: r.parseString()) @@ -428,7 +452,7 @@ proc parseJsonNode(r: var JsonReader): JsonNode = r.toInt(val, typeof(result.num), JsonReaderFlag.portableInt in r.lex.flags)) of JsonValueKind.Object: result = JsonNode(kind: JObject) - parseObjectImpl(r.lex): discard # initial action + parseObjectImpl(r.lex, skipNullFields): discard # initial action do: discard # closing action do: discard # comma action do: # key action diff --git a/tests/test_json_flavor.nim b/tests/test_json_flavor.nim index 926c802..4ac2982 100644 --- a/tests/test_json_flavor.nim +++ b/tests/test_json_flavor.nim @@ -51,6 +51,9 @@ type three: JsonNumber[string] four: JsonValueRef[uint64] + ListOnly = object + list: JsonString + Container.useDefaultSerializationIn StringyJson createJsonFlavor OptJson @@ -71,7 +74,20 @@ const } } } + """ + jsonTextWithNullFields = """ +{ + "list": null +} +""" + +createJsonFlavor NullyFields, + skipNullFields = true, + requireAllFields = false + +Container.useDefaultSerializationIn NullyFields +ListOnly.useDefaultSerializationIn NullyFields suite "Test JsonFlavor": test "basic test": @@ -104,3 +120,15 @@ suite "Test JsonFlavor": check: ww == vv xx == """{"two":-789.0009e-19,"three":999.776000e33,"four":{"apple":[1,true,"three"],"banana":{"chip":123,"z":null,"v":false}}}""" + + test "object with null fields": + expect JsonReaderError: + let x = Json.decode(jsonTextWithNullFields, Container) + discard x + + let x = NullyFields.decode(jsonTextWithNullFields, Container) + check x.list.len == 0 + + # field should not processed at all + let y = NullyFields.decode(jsonTextWithNullFields, ListOnly) + check y.list.string.len == 0