diff --git a/json_serialization/reader.nim b/json_serialization/reader.nim index bc97337..a579977 100644 --- a/json_serialization/reader.nim +++ b/json_serialization/reader.nim @@ -14,6 +14,7 @@ type JsonReader*[Flavor = DefaultFlavor] = object lexer*: JsonLexer allowUnknownFields: bool + requireAllFields: bool JsonReaderError* = object of JsonError line*, col*: int @@ -46,6 +47,9 @@ type UnexpectedValueError* = object of JsonReaderError + IncompleteObjectError* = object of JsonReaderError + objectType: cstring + IntOverflowError* = object of JsonReaderError isNegative: bool absIntVal: uint64 @@ -79,6 +83,9 @@ method formatMsg*(err: ref IntOverflowError, filename: string): string = method formatMsg*(err: ref UnexpectedValueError, filename: string): string = tryFmt: fmt"{filename}({err.line}, {err.col}) {err.msg}" +method formatMsg*(err: ref IncompleteObjectError, filename: string): string = + tryFmt: fmt"{filename}({err.line}, {err.col}) Not all required fields were specified when reading '{err.objectType}'" + proc assignLineNumber*(ex: ref JsonReaderError, r: JsonReader) = ex.line = r.lexer.line ex.col = r.lexer.tokenStartCol @@ -111,6 +118,12 @@ proc raiseUnexpectedField*(r: JsonReader, fieldName, deserializedType: cstring) ex.deserializedType = deserializedType raise ex +proc raiseIncompleteObject*(r: JsonReader, objectType: cstring) {.noreturn.} = + var ex = new IncompleteObjectError + ex.assignLineNumber(r) + ex.objectType = objectType + raise ex + proc handleReadException*(r: JsonReader, Record: type, fieldName: string, @@ -125,8 +138,10 @@ proc handleReadException*(r: JsonReader, proc init*(T: type JsonReader, stream: InputStream, mode = defaultJsonMode, - allowUnknownFields = false): T = + allowUnknownFields = false, + requireAllFields = false): T = result.allowUnknownFields = allowUnknownFields + result.requireAllFields = requireAllFields result.lexer = JsonLexer.init(stream, mode) result.lexer.next() @@ -500,7 +515,9 @@ proc readValue*[T](r: var JsonReader, value: var T) type T = type(value) r.skipToken tkCurlyLe - when T.totalSerializedFields > 0: + const expectedFields = T.totalSerializedFields + var readFields = 0 + when expectedFields > 0: let fields = T.fieldReadersTable(ReaderType) var expectedFieldPos = 0 while r.lexer.tok == tkString: @@ -513,6 +530,7 @@ proc readValue*[T](r: var JsonReader, value: var T) r.skipToken tkColon if reader != nil: reader(value, r) + inc readFields elif r.allowUnknownFields: r.skipSingleJsValue() else: @@ -523,6 +541,10 @@ proc readValue*[T](r: var JsonReader, value: var T) else: break + if r.requireAllFields and readFields != expectedFields: + const typeName = typetraits.name(T) + r.raiseIncompleteObject(typeName) + r.skipToken tkCurlyRi else: diff --git a/tests/test_serialization.nim b/tests/test_serialization.nim index 55fe0a7..9e7e16f 100644 --- a/tests/test_serialization.nim +++ b/tests/test_serialization.nim @@ -132,6 +132,85 @@ suite "toJson tests": let shouldNotDecode = Json.decode(json, Simple) echo "This should not have decoded ", shouldNotDecode + test "all fields are required and present": + let json = dedent""" + { + "x": 20, + "distance": 10, + "y": "y value" + } + """ + + let decoded = Json.decode(json, Simple, requireAllFields = true) + + check: + decoded.x == 20 + decoded.y == "y value" + decoded.distance.int == 10 + + test "all fields were required, but not all were provided": + let json = dedent""" + { + "x": -20, + "distance": 10 + } + """ + + expect IncompleteObjectError: + let shouldNotDecode = Json.decode(json, Simple, requireAllFields = true) + echo "This should not have decoded ", shouldNotDecode + + test "all fields were required, but not all were provided (additional fields present instead)": + let json = dedent""" + { + "futureBool": false, + "y": "y value", + "futureObject": {"a": -1, "b": [1, 2.0, 3.1], "c": null, "d": true}, + "distance": 10 + } + """ + + expect IncompleteObjectError: + let shouldNotDecode = Json.decode(json, Simple, + requireAllFields = true, + allowUnknownFields = true) + echo "This should not have decoded ", shouldNotDecode + + test "all fields were required, but none were provided": + let json = "{}" + + expect IncompleteObjectError: + let shouldNotDecode = Json.decode(json, Simple, requireAllFields = true) + echo "This should not have decoded ", shouldNotDecode + + test "all fields are required and provided, and additional ones are present": + let json = dedent""" + { + "x": 20, + "distance": 10, + "futureBool": false, + "y": "y value", + "futureObject": {"a": -1, "b": [1, 2.0, 3.1], "c": null, "d": true}, + } + """ + + let decoded = try: + Json.decode(json, Simple, requireAllFields = true, allowUnknownFields = true) + except SerializationError as err: + checkpoint "Unexpected deserialization failure: " & err.formatMsg("") + raise + + check: + decoded.x == 20 + decoded.y == "y value" + decoded.distance.int == 10 + + expect UnexpectedField: + let shouldNotDecode = Json.decode(json, Simple, + requireAllFields = true, + allowUnknownFields = false) + echo "This should not have decoded ", shouldNotDecode + test "arrays are printed correctly": var x = HoldsArray(data: @[1, 2, 3, 4])