From ffd7f08dbc4cf4e0ca09a3991ab3bc54e5b4ace9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Sat, 29 Aug 2026 13:59:56 -0400 Subject: [PATCH 1/7] json: print location with parsing errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- lib/json/error.nit | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/json/error.nit b/lib/json/error.nit index f13d2fc22b..83119fd658 100644 --- a/lib/json/error.nit +++ b/lib/json/error.nit @@ -20,4 +20,13 @@ class JsonParseError # Location of the error in source var location: nullable Location = null + + redef fun to_s do + var loc = location + if loc != null then + return "{loc}: {super}" + else + return super + end + end end From 61c5bb33008a2cb9de36b57dd0f7bd8ae495c40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Sat, 29 Aug 2026 13:59:56 -0400 Subject: [PATCH 2/7] json::static: update and improve doc of parse_json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- lib/json/static.nit | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/json/static.nit b/lib/json/static.nit index 2d42e161eb..dc418be196 100644 --- a/lib/json/static.nit +++ b/lib/json/static.nit @@ -90,14 +90,15 @@ redef class Text return res.to_s end - # Parse `self` as JSON. + # Parse `self` from JSON to an object. # - # If `self` is not a valid JSON document or contains an unsupported escape + # This feature accepts partial JSON entities, not just full JSON objects. + # If `self` is not a valid JSON entity or contains an unsupported escape # sequence, return a `JSONParseError`. # # Example with `JsonObject`: # - # var obj = "\{\"foo\": \{\"bar\": true, \"goo\": [1, 2, 3]\}\}".parse_json + # var obj = """{"foo": {"bar": true, "goo": [1, 2, 3]}}""".parse_json # assert obj isa JsonObject # assert obj["foo"] isa JsonObject # assert obj["foo"].as(JsonObject)["bar"] == true @@ -116,11 +117,17 @@ redef class Text # assert str isa String # assert str == "foo, bar, baz" # - # Example of a syntax error: + # Example of a syntax error on an invalid key: # - # var error = "\{foo: \"bar\"\}".parse_json - # assert error isa JsonParseError - # assert error.to_s == "Bad key format Error: bad JSON entity" + # var key_error = "\{42: \"bar\"\}".parse_json + # assert key_error isa JsonParseError + # assert key_error.to_s == "line 1, position 4: expected string as JSON key, got '42'" + # + # Example of a syntax error on a truncated string: + # + # var truncated_error = "\{\"key\": \"trucat".parse_json + # assert truncated_error isa JsonParseError + # assert truncated_error.to_s == "line 1, position 9: truncated JSON string, after 't'" fun parse_json: nullable Serializable do return (new JSONStringParser(self.to_s)).parse_entity end From 5351380963ba0d8faa35a7b5b3d0e358804370bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Sat, 29 Aug 2026 13:59:56 -0400 Subject: [PATCH 3/7] json::static: fix missing return making some error states ineffective MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- lib/json/static.nit | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/json/static.nit b/lib/json/static.nit index dc418be196..330391c0df 100644 --- a/lib/json/static.nit +++ b/lib/json/static.nit @@ -192,21 +192,21 @@ class JSONStringParser pos += 1 return parse_json_object else if c == 'f' then - if pos + 4 >= srclen then make_parse_error("Error: bad JSON entity") + if pos + 4 >= srclen then return make_parse_error("Error: bad JSON entity") if src[pos + 1] == 'a' and src[pos + 2] == 'l' and src[pos + 3] == 's' and src[pos + 4] == 'e' then pos += 5 return false end return make_parse_error("Error: bad JSON entity") else if c == 't' then - if pos + 3 >= srclen then make_parse_error("Error: bad JSON entity") + if pos + 3 >= srclen then return make_parse_error("Error: bad JSON entity") if src[pos + 1] == 'r' and src[pos + 2] == 'u' and src[pos + 3] == 'e' then pos += 4 return true end return make_parse_error("Error: bad JSON entity") else if c == 'n' then - if pos + 3 >= srclen then make_parse_error("Error: bad JSON entity") + if pos + 3 >= srclen then return make_parse_error("Error: bad JSON entity") if src[pos + 1] == 'u' and src[pos + 2] == 'l' and src[pos + 3] == 'l' then pos += 4 return null @@ -391,7 +391,7 @@ class JSONStringParser p += 1 for i in [0 .. 4[ do cp <<= 4 - if p >= ln then make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + if p >= ln then return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") c = src[p] if c >= '0' and c <= '9' then cp += c.code_point - '0'.code_point @@ -400,23 +400,23 @@ class JSONStringParser else if c >= 'A' and c <= 'F' then cp += c.code_point - 'A'.code_point + 10 else - make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") end p += 1 end c = cp.code_point if cp >= 0xD800 and cp <= 0xDBFF then - if p >= ln then make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + if p >= ln then return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") c = src[p] - if c != '\\' then make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + if c != '\\' then return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") p += 1 c = src[p] - if c != 'u' then make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + if c != 'u' then return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") var locp = 0 p += 1 for i in [0 .. 4[ do locp <<= 4 - if p > ln then make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + if p > ln then return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") c = src[p] if c >= '0' and c <= '9' then locp += c.code_point - '0'.code_point @@ -425,7 +425,7 @@ class JSONStringParser else if c >= 'A' and c <= 'F' then locp += c.code_point - 'A'.code_point + 10 else - make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") end p += 1 end From c7a87721505bdf08b30c155450add9ca924dd931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Sat, 29 Aug 2026 13:59:56 -0400 Subject: [PATCH 4/7] json::static: move some services to protected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- lib/json/static.nit | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/json/static.nit b/lib/json/static.nit index 330391c0df..4f2ceca335 100644 --- a/lib/json/static.nit +++ b/lib/json/static.nit @@ -218,7 +218,7 @@ class JSONStringParser end # Parses a JSON Array - fun parse_json_array: Serializable do + protected fun parse_json_array: Serializable do var max = len if pos >= max then return make_parse_error("Incomplete JSON array") var arr = new JsonArray @@ -283,7 +283,7 @@ class JSONStringParser end # Parses an Int or Float - fun parse_json_number: Serializable do + protected fun parse_json_number: Serializable do var max = len var p = pos var c = src[p] @@ -357,7 +357,7 @@ class JSONStringParser private var parse_str_buf = new FlatBuffer # Parses and returns a Nit string from a JSON String - fun parse_json_string: Serializable do + protected fun parse_json_string: Serializable do var src = src var ln = src.length var p = pos @@ -454,7 +454,7 @@ class JSONStringParser end # Ignores any character until a JSON separator is encountered - fun ignore_until_separator do + protected fun ignore_until_separator do var max = len while pos < max do if not src[pos].is_json_separator then return From 95bf338ce9bdf99af4c6344b91a6ee6048721893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Mon, 31 Aug 2026 10:41:08 -0400 Subject: [PATCH 5/7] json::static: align error message style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- lib/json/static.nit | 83 ++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/lib/json/static.nit b/lib/json/static.nit index 4f2ceca335..b78c98aa2e 100644 --- a/lib/json/static.nit +++ b/lib/json/static.nit @@ -125,9 +125,14 @@ redef class Text # # Example of a syntax error on a truncated string: # - # var truncated_error = "\{\"key\": \"trucat".parse_json - # assert truncated_error isa JsonParseError - # assert truncated_error.to_s == "line 1, position 9: truncated JSON string, after 't'" + # assert """{"key": "trucate""".parse_json.to_s == + # "line 1, position 9: truncated JSON string, after 'e'" + # assert """{"key": tru""".parse_json.to_s == + # "line 1, position 9: truncated 'true' keyword" + # assert """{"key":""".parse_json.to_s == + # "line 1, position 8: reached end of input string" + # assert """{"ke""".parse_json.to_s == + # "line 1, position 2: expected string as JSON key, got parsing error 'truncated JSON string, after 'e''" fun parse_json: nullable Serializable do return (new JSONStringParser(self.to_s)).parse_entity end @@ -180,7 +185,7 @@ class JSONStringParser fun parse_entity: nullable Serializable do var srclen = len ignore_whitespaces - if pos >= srclen then return make_parse_error("Empty JSON") + if pos >= srclen then return make_parse_error("reached end of input string") var c = src[pos] if c == '[' then pos += 1 @@ -192,50 +197,50 @@ class JSONStringParser pos += 1 return parse_json_object else if c == 'f' then - if pos + 4 >= srclen then return make_parse_error("Error: bad JSON entity") + if pos + 4 >= srclen then return make_parse_error("truncated 'false' keyword") if src[pos + 1] == 'a' and src[pos + 2] == 'l' and src[pos + 3] == 's' and src[pos + 4] == 'e' then pos += 5 return false end - return make_parse_error("Error: bad JSON entity") + return make_parse_error("expected 'false', got '{src.substring(pos, 5)}'") else if c == 't' then - if pos + 3 >= srclen then return make_parse_error("Error: bad JSON entity") + if pos + 3 >= srclen then return make_parse_error("truncated 'true' keyword") if src[pos + 1] == 'r' and src[pos + 2] == 'u' and src[pos + 3] == 'e' then pos += 4 return true end - return make_parse_error("Error: bad JSON entity") + return make_parse_error("expected 'true', got '{src.substring(pos, 4)}'") else if c == 'n' then - if pos + 3 >= srclen then return make_parse_error("Error: bad JSON entity") + if pos + 3 >= srclen then return make_parse_error("truncated 'null' keyword") if src[pos + 1] == 'u' and src[pos + 2] == 'l' and src[pos + 3] == 'l' then pos += 4 return null end - return make_parse_error("Error: bad JSON entity") + return make_parse_error("expected 'null', got '{src.substring(pos, 4)}'") end - if not c.is_json_num_start then return make_parse_error("Bad JSON character") + if not c.is_json_num_start then return make_parse_error("expected start of JSON number, got '{c}'") return parse_json_number end # Parses a JSON Array protected fun parse_json_array: Serializable do var max = len - if pos >= max then return make_parse_error("Incomplete JSON array") + if pos >= max then return make_parse_error("JSON array truncated") var arr = new JsonArray var c = src[pos] while not c == ']' do ignore_whitespaces - if pos >= max then return make_parse_error("Incomplete JSON array") + if pos >= max then return make_parse_error("JSON array truncated") if src[pos] == ']' then break var ent = parse_entity #print "Parsed an entity {ent} for a JSON array" if ent isa JsonParseError then return ent arr.add ent ignore_whitespaces - if pos >= max then return make_parse_error("Incomplete JSON array") + if pos >= max then return make_parse_error("JSON array truncated") c = src[pos] if c == ']' then break - if c != ',' then return make_parse_error("Bad array separator {c}") + if c != ',' then return make_parse_error("expected JSON array separator ',' or ']', got '{c}'") pos += 1 end pos += 1 @@ -245,30 +250,32 @@ class JSONStringParser # Parses a JSON Object fun parse_json_object: Serializable do var max = len - if pos >= max then return make_parse_error("Incomplete JSON object") + if pos >= max then return make_parse_error("truncated JSON object") var obj = new JsonObject var c = src[pos] while not c == '}' do ignore_whitespaces - if pos >= max then return make_parse_error("Malformed JSON object") + if pos >= max then return make_parse_error("truncated JSON object") if src[pos] == '}' then break var key = parse_entity #print "Parsed key {key} for JSON object" - if not key isa String then return make_parse_error("Bad key format {key or else "null"}") + if key isa JsonParseError then return make_parse_error("expected string as JSON key, got parsing error '{key.message}'") + if not key isa String then return make_parse_error("expected string as JSON key, got '{key or else "null"}'") ignore_whitespaces - if pos >= max then return make_parse_error("Incomplete JSON object") - if not src[pos] == ':' then return make_parse_error("Bad key/value separator {src[pos]}") + if pos >= max then return make_parse_error("truncated JSON object") + if not src[pos] == ':' then return make_parse_error("expected JSON key/value separator ':', got '{src[pos]}'") pos += 1 ignore_whitespaces var value = parse_entity #print "Parsed value {value} for JSON object" + # Pass this error directly, don't nest it. It could be many objects deep. if value isa JsonParseError then return value obj[key] = value ignore_whitespaces - if pos >= max then return make_parse_error("Incomplete JSON object") + if pos >= max then return make_parse_error("truncated JSON object") c = src[pos] if c == '}' then break - if c != ',' then return make_parse_error("Bad object separator {src[pos]}") + if c != ',' then return make_parse_error("expected JSON separator ',', got '{c}'") pos += 1 end pos += 1 @@ -291,7 +298,7 @@ class JSONStringParser if c == '-' then is_neg = true p += 1 - if p >= max then return make_parse_error("Bad JSON number") + if p >= max then return make_parse_error("truncated JSON number after '{c}'") c = src[p] end var val = 0 @@ -304,7 +311,7 @@ class JSONStringParser end if c == '.' then p += 1 - if p >= max then return make_parse_error("Bad JSON number") + if p >= max then return make_parse_error("truncated JSON number after '{c}'") c = src[p] var fl = val.to_f var frac = 0.1 @@ -318,7 +325,7 @@ class JSONStringParser if c == 'e' or c == 'E' then p += 1 var exp = 0 - if p >= max then return make_parse_error("Malformed JSON number") + if p >= max then return make_parse_error("truncated JSON number after '{c}'") c = src[p] while c.is_numeric do exp *= 10 @@ -329,14 +336,14 @@ class JSONStringParser end fl *= (10 ** exp).to_f end - if p < max and not c.is_json_separator then return make_parse_error("Malformed JSON number") + if p < max and not c.is_json_separator then return make_parse_error("expected JSON number or separator character, got '{c}'") pos = p if is_neg then return -fl return fl end if c == 'e' or c == 'E' then p += 1 - if p >= max then return make_parse_error("Bad JSON number") + if p >= max then return make_parse_error("truncated JSON number after '{c}'") var exp = src[p].to_i c = src[p] while c.is_numeric do @@ -348,7 +355,7 @@ class JSONStringParser end val *= (10 ** exp) end - if p < max and not src[p].is_json_separator then return make_parse_error("Malformed JSON number") + if p < max and not src[p].is_json_separator then return make_parse_error("truncated JSON number") pos = p if is_neg then return -val return val @@ -362,20 +369,20 @@ class JSONStringParser var ln = src.length var p = pos p += 1 - if p > ln then return make_parse_error("Malformed JSON String") + if p > ln then return make_parse_error("truncated JSON string") var c = src[p] var ret = parse_str_buf var chunk_st = p while c != '"' do if c != '\\' then p += 1 - if p >= ln then return make_parse_error("Malformed JSON string") + if p >= ln then return make_parse_error("truncated JSON string, after '{c}'") c = src[p] continue end ret.append_substring_impl(src, chunk_st, p - chunk_st) p += 1 - if p >= ln then return make_parse_error("Malformed Escape sequence in JSON string") + if p >= ln then return make_parse_error("truncated escape sequence in JSON string") c = src[p] if c == 'r' then ret.add '\r' @@ -391,7 +398,7 @@ class JSONStringParser p += 1 for i in [0 .. 4[ do cp <<= 4 - if p >= ln then return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + if p >= ln then return make_parse_error("truncated \uXXXX Escape sequence in JSON string") c = src[p] if c >= '0' and c <= '9' then cp += c.code_point - '0'.code_point @@ -400,23 +407,23 @@ class JSONStringParser else if c >= 'A' and c <= 'F' then cp += c.code_point - 'A'.code_point + 10 else - return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + return make_parse_error("truncated \uXXXX Escape sequence in JSON string") end p += 1 end c = cp.code_point if cp >= 0xD800 and cp <= 0xDBFF then - if p >= ln then return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + if p >= ln then return make_parse_error("truncated \uXXXX JSON escape sequence") c = src[p] - if c != '\\' then return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + if c != '\\' then return make_parse_error("expected '\' in \uXXXX JSON escape sequence, got '{c}'") p += 1 c = src[p] - if c != 'u' then return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + if c != 'u' then return make_parse_error("expected 'u' in \uXXXX JSON escape sequence, got '{c}'") var locp = 0 p += 1 for i in [0 .. 4[ do locp <<= 4 - if p > ln then return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + if p > ln then return make_parse_error("truncated \uXXXX JSON escape sequence") c = src[p] if c >= '0' and c <= '9' then locp += c.code_point - '0'.code_point @@ -425,7 +432,7 @@ class JSONStringParser else if c >= 'A' and c <= 'F' then locp += c.code_point - 'A'.code_point + 10 else - return make_parse_error("Malformed \uXXXX Escape sequence in JSON string") + return make_parse_error("expected hexadecimal digit in \uXXXX JSON escape, got '{c}'") end p += 1 end From 4a8057c82be56273d83352beb8deb470f49e3d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Sat, 29 Aug 2026 13:59:56 -0400 Subject: [PATCH 6/7] json::static: update expected error messages in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Alexis Laferrière --- tests/sav/nitwebcrawl.res | 2 +- tests/sav/test_json_deserialization_plain.res | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sav/nitwebcrawl.res b/tests/sav/nitwebcrawl.res index 7e723de4ae..959a3abcf9 100644 --- a/tests/sav/nitwebcrawl.res +++ b/tests/sav/nitwebcrawl.res @@ -1,3 +1,3 @@ process http://localhost:3000/api/entity/core. 1+0/1 Error with http://localhost:3000/api/entity/core -http://localhost:3000/api/entity/core: Empty JSON +http://localhost:3000/api/entity/core: reached end of input string diff --git a/tests/sav/test_json_deserialization_plain.res b/tests/sav/test_json_deserialization_plain.res index 8487e43d97..629fa9b836 100644 --- a/tests/sav/test_json_deserialization_plain.res +++ b/tests/sav/test_json_deserialization_plain.res @@ -26,6 +26,6 @@ # Nit: > # JSON: not valid json -# Errors: 'Error: bad JSON entity' +# Errors: 'line 1, position 1: expected 'null', got 'not '' # Nit: null From 179d9e5b17c79f57b85cd230264e102f3f9cc362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Laferri=C3=A8re?= Date: Mon, 31 Aug 2026 11:38:16 -0400 Subject: [PATCH 7/7] json::static: remove broken and unused `ignore_until_separator` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loop never advances `pos` but it has no callers anyway, let's remove it. Signed-off-by: Alexis Laferrière --- lib/json/static.nit | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/json/static.nit b/lib/json/static.nit index b78c98aa2e..764cad625d 100644 --- a/lib/json/static.nit +++ b/lib/json/static.nit @@ -459,14 +459,6 @@ class JSONStringParser ret.clear return rets end - - # Ignores any character until a JSON separator is encountered - protected fun ignore_until_separator do - var max = len - while pos < max do - if not src[pos].is_json_separator then return - end - end end # A map that can be translated into a JSON object.