Last active
January 14, 2026 07:55
-
-
Save UradaSources/ebadc6930ef13c65238c9efef089526d to your computer and use it in GitHub Desktop.
Single file C# JSON streaming parser, without dependencies
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| Copyright 2026/1/14 Urada urada@foxmail.com | |
| Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), | |
| to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, | |
| distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, | |
| subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | |
| DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| */ | |
| using System.Collections; | |
| using System.Collections.Generic; | |
| using System.Text; | |
| // 警告: 该解析器的开发仅用了数个小时, 未经充分测试 | |
| // 用于解析流式JSON数据的JSON词法与语法解析器 | |
| // 该解析器尽可能的从输入的JSON字符串中解析有效的JSON对象直到发生错误, 非常适合解析流式JSON文本 | |
| // 若有可能, 该解析器将会对未封闭的JSON元素进行封闭, 包括字符串, 对象和数组 | |
| // 对于key value pair有特殊的规则, 仅允许key为字符串, 且当key是未封闭的字符串对象时, 将会被忽略且解析立刻结束 | |
| // 使用JSONElement::ToString()可以将JSON对象重新序列化为文本 | |
| // 例子: | |
| // var queue = new Queue<JSONToken>(); | |
| // JSONElement json; | |
| // | |
| // var parser = new JSON(); | |
| // parser.Lexer(m_test, queue); | |
| // var exc = parser.Parser(queue, out json); | |
| // | |
| // var text = json.ToString(); | |
| // Debug.Log(text); | |
| // Debug.LogException(exc); | |
| // 示例输出: | |
| // IN OUT | |
| // { "a {} | |
| // {"a":"12 {"a":"12"} | |
| // { "b":123, "a {"b":123} | |
| namespace JSONStremming | |
| { | |
| public enum JSONTokenType | |
| { | |
| ObjectBegin, // { | |
| ObjectEnd, // } | |
| ArrayBegin, // [ | |
| ArrayEnd, // ] | |
| Colon, // : | |
| Comma, // , | |
| String, | |
| BoolAndNumber, | |
| } | |
| public struct JSONToken | |
| { | |
| public JSONTokenType type; | |
| public string value; | |
| public override string ToString() | |
| { | |
| switch (this.type) | |
| { | |
| case JSONTokenType.BoolAndNumber: | |
| case JSONTokenType.String: | |
| return $"({type}){value}"; | |
| default: | |
| return value; | |
| } | |
| } | |
| } | |
| public enum JSONParseError | |
| { | |
| InvalidKeyValuePair, | |
| InvalidValue, | |
| MissingComma, | |
| ObjectParseError, | |
| ArrayParseError, | |
| } | |
| public class JSONParseErrorStack | |
| { | |
| public Stack<JSONParseError> stack { get; } = new Stack<JSONParseError>(); | |
| public readonly string message; | |
| public string Message => $"JSON Parse Error: {message}. Stack: {string.Join(',', this.stack)}"; | |
| public JSONParseErrorStack(JSONParseError state, string message = "") | |
| { | |
| this.stack.Push(state); | |
| this.message = message; | |
| } | |
| } | |
| public class JSONElement | |
| { | |
| public enum Type | |
| { | |
| Nil, | |
| String, | |
| Number, | |
| Bool, | |
| Object, | |
| Array | |
| } | |
| public Type type { protected set; get; } = Type.Nil; | |
| public readonly string str = string.Empty; | |
| public readonly float number = 0; | |
| public readonly bool boolval = false; | |
| public bool isNull => this.type == Type.Nil; | |
| public bool isString => this.type == Type.String; | |
| public bool isNumber => this.type == Type.Number; | |
| public bool isBool => this.type == Type.Bool; | |
| public bool isObject => this.type == Type.Object; | |
| public bool isArray => this.type == Type.Array; | |
| public JSONElement() | |
| { | |
| this.type = Type.Nil; | |
| } | |
| public JSONElement(string str) | |
| { | |
| this.type = Type.String; | |
| this.str = str; | |
| } | |
| public JSONElement(float number) | |
| { | |
| this.type = Type.Number; | |
| this.number = number; | |
| } | |
| public JSONElement(bool boolval) | |
| { | |
| this.type = Type.Bool; | |
| this.boolval = boolval; | |
| } | |
| public static explicit operator string(JSONElement element) | |
| { | |
| if (element.type != Type.String) | |
| throw new System.InvalidCastException(); | |
| return element.str; | |
| } | |
| public static explicit operator float(JSONElement element) | |
| { | |
| if (element.type != Type.Number) | |
| throw new System.InvalidCastException(); | |
| return element.number; | |
| } | |
| public static explicit operator bool(JSONElement element) | |
| { | |
| if (element.type != Type.Bool) | |
| throw new System.InvalidCastException(); | |
| return element.boolval; | |
| } | |
| public override string ToString() | |
| { | |
| switch (this.type) | |
| { | |
| case Type.Nil: | |
| return "null"; | |
| case Type.String: | |
| return '"' + this.str + '"'; | |
| case Type.Number: | |
| return this.number.ToString(); | |
| case Type.Bool: | |
| return this.boolval ? "true" : "false"; | |
| case Type.Object: | |
| return (this as JSONObject).ToString(); | |
| case Type.Array: | |
| return (this as JSONArray).ToString(); | |
| } | |
| return base.ToString(); | |
| } | |
| } | |
| public class JSONObject : JSONElement | |
| { | |
| public Dictionary<string, JSONElement> dict { get; } = new Dictionary<string, JSONElement>(); | |
| public JSONObject() | |
| { | |
| this.type = Type.Object; | |
| } | |
| public override string ToString() | |
| { | |
| var builder = new StringBuilder(); | |
| builder.Append('{'); | |
| bool first = true; | |
| foreach (var pair in this.dict) | |
| { | |
| if (first) | |
| first = false; | |
| else | |
| builder.Append(','); | |
| builder.Append('"' + pair.Key + '"'); | |
| builder.Append(':'); | |
| builder.Append(pair.Value.ToString()); | |
| } | |
| builder.Append('}'); | |
| return builder.ToString(); | |
| } | |
| } | |
| public class JSONArray : JSONElement | |
| { | |
| public List<JSONElement> array { get; } = new List<JSONElement>(); | |
| public JSONArray() | |
| { | |
| this.type = Type.Array; | |
| } | |
| public override string ToString() | |
| { | |
| var builder = new StringBuilder(); | |
| builder.Append('['); | |
| bool first = true; | |
| foreach (var item in this.array) | |
| { | |
| if (first) | |
| first = false; | |
| else | |
| builder.Append(','); | |
| builder.Append(item.ToString()); | |
| } | |
| builder.Append(']'); | |
| return builder.ToString(); | |
| } | |
| } | |
| public class JSONParser | |
| { | |
| // json ebnf | |
| // top ::= [value] | |
| // value ::= <string> | <boolOrNumber> | array | object | |
| // pair ::= <string> ':' value | |
| // object ::= '{' [pair {',' pair}] '}' | |
| // array ::= '[' [value {',' value}] ']' | |
| public int Lexer(string json, Queue<JSONToken> tokens) | |
| { | |
| if (string.IsNullOrWhiteSpace(json)) | |
| return 0; | |
| int origCount = tokens.Count; | |
| bool escapeOnString = false; // 上一个字符是否为转义字符 | |
| int stringLiteralStartPos = -1; // 字符串字面量解析起始位 | |
| int boolOrNumberLiteralStartPos = -1; // 非字符串字面量解析起始位 | |
| var symbolSet = new Dictionary<char, JSONTokenType>() { | |
| { '{', JSONTokenType.ObjectBegin }, | |
| { '}', JSONTokenType.ObjectEnd }, | |
| { '[', JSONTokenType.ArrayBegin }, | |
| { ']', JSONTokenType.ArrayEnd }, | |
| { ':', JSONTokenType.Colon }, | |
| { ',', JSONTokenType.Comma }, | |
| }; | |
| for (int i = 0; i < json.Length; i++) | |
| { | |
| char c = json[i]; | |
| // 优先对字符串进行处理 | |
| if (stringLiteralStartPos >= 0) | |
| { | |
| if (c == '"' && !escapeOnString) | |
| { | |
| int start = stringLiteralStartPos + 1; | |
| int lenght = i - start; | |
| // 注意去除引号, boolOrNumberLiteral则不需要 | |
| string value = json.Substring(start, lenght); | |
| tokens.Enqueue(new JSONToken { type = JSONTokenType.String, value = value }); | |
| stringLiteralStartPos = -1; | |
| } | |
| else // 检查转义字符 | |
| { | |
| escapeOnString = (c == '\\' && !escapeOnString); | |
| } | |
| } | |
| else if (c == '"') | |
| { | |
| stringLiteralStartPos = i; | |
| } | |
| // 检查其他符号 | |
| else if (symbolSet.TryGetValue(c, out var t)) | |
| { | |
| // 终结布尔与数值词法单元 | |
| if (boolOrNumberLiteralStartPos >= 0) | |
| { | |
| int start = boolOrNumberLiteralStartPos; | |
| int lenght = i - start; | |
| string value = json.Substring(start, lenght); | |
| tokens.Enqueue(new JSONToken { type = JSONTokenType.BoolAndNumber, value = value }); | |
| boolOrNumberLiteralStartPos = -1; | |
| } | |
| tokens.Enqueue(new JSONToken { type = t, value = c.ToString() }); | |
| continue; | |
| } | |
| else if (char.IsWhiteSpace(c)) | |
| { | |
| // 终结布尔与数值词法单元 | |
| if (boolOrNumberLiteralStartPos >= 0) | |
| { | |
| int start = boolOrNumberLiteralStartPos; | |
| int lenght = i - start; | |
| string value = json.Substring(start, lenght); | |
| tokens.Enqueue(new JSONToken { type = JSONTokenType.BoolAndNumber, value = value }); | |
| boolOrNumberLiteralStartPos = -1; | |
| } | |
| } | |
| // 尝试开始解析布尔与数值词法单元 | |
| else if (char.IsDigit(c) || (c >= 'a' && c <= 'z') || c == '\\') | |
| { | |
| if (boolOrNumberLiteralStartPos < 0) | |
| boolOrNumberLiteralStartPos = i; | |
| } | |
| else | |
| { | |
| throw new System.Exception("Parser faild, invaild char: " + c); | |
| } | |
| } | |
| // 尝试闭合字面量值 | |
| if (stringLiteralStartPos >= 0) | |
| { | |
| int start = stringLiteralStartPos + 1; | |
| int length = json.Length - start; | |
| // 注意去除引号, boolOrNumberLiteral则不需要 | |
| if (escapeOnString) | |
| length -= 1; | |
| string value = json.Substring(start, length); | |
| tokens.Enqueue(new JSONToken { type = JSONTokenType.String, value = value }); | |
| stringLiteralStartPos = -1; | |
| } | |
| if (boolOrNumberLiteralStartPos >= 0) | |
| { | |
| int start = boolOrNumberLiteralStartPos; | |
| int length = json.Length - start; | |
| string value = json.Substring(start, length); | |
| tokens.Enqueue(new JSONToken { type = JSONTokenType.BoolAndNumber, value = value }); | |
| boolOrNumberLiteralStartPos = -1; | |
| } | |
| return tokens.Count - origCount; | |
| } | |
| public JSONParseErrorStack Parser(Queue<JSONToken> tokens, out JSONElement result) | |
| { | |
| result = new JSONElement(); | |
| if (tokens.Count == 0) | |
| return null; | |
| JSONParseErrorStack error = null; | |
| if (tokens.Peek().type == JSONTokenType.ObjectBegin) | |
| error = this.ParserObject(tokens, out result); | |
| else if (tokens.Peek().type == JSONTokenType.ArrayBegin) | |
| error = this.ParserArray(tokens, out result); | |
| else | |
| error = this.ParserValue(tokens, out result); | |
| return error; | |
| } | |
| private JSONParseErrorStack ParserObject(Queue<JSONToken> tokens, out JSONElement result) | |
| { | |
| if (tokens.Count == 0) | |
| { | |
| result = null; | |
| return new JSONParseErrorStack(JSONParseError.ObjectParseError); | |
| } | |
| if (tokens.Peek().type == JSONTokenType.ObjectBegin) | |
| tokens.Dequeue(); | |
| var typedResult = new JSONObject(); | |
| result = typedResult; | |
| bool hasMorePairs = false; | |
| while (tokens.Count > 0) | |
| { | |
| // 遇到 } 结束解析 | |
| if (tokens.Peek().type == JSONTokenType.ObjectEnd) | |
| break; | |
| // 处理2个键值对之间的逗号分隔 | |
| if (hasMorePairs) | |
| { | |
| if (tokens.Peek().type == JSONTokenType.Comma) | |
| tokens.Dequeue(); | |
| else | |
| return new JSONParseErrorStack(JSONParseError.MissingComma); | |
| } | |
| // 解析单个键值对 | |
| var error = this.ParserKeyValuePair(tokens, out string key, out JSONElement value); | |
| if (key != null && value != null) | |
| typedResult.dict.Add(key, value); | |
| if (error != null) | |
| { | |
| error.stack.Push(JSONParseError.ObjectParseError); | |
| return error; | |
| } | |
| hasMorePairs = true; | |
| } | |
| if (tokens.Count > 0 && tokens.Peek().type == JSONTokenType.ObjectEnd) | |
| tokens.Dequeue(); | |
| else | |
| return new JSONParseErrorStack(JSONParseError.ObjectParseError); | |
| return null; | |
| } | |
| private JSONParseErrorStack ParserArray(Queue<JSONToken> tokens, out JSONElement result) | |
| { | |
| if (tokens.Count == 0) | |
| { | |
| result = null; | |
| return new JSONParseErrorStack(JSONParseError.ObjectParseError); | |
| } | |
| // 修正:数组开头是 ArrayBegin 而非 ObjectBegin | |
| if (tokens.Peek().type == JSONTokenType.ArrayBegin) | |
| tokens.Dequeue(); | |
| var typedResult = new JSONArray(); | |
| result = typedResult; | |
| bool hasMoreValues = false; | |
| while (tokens.Count > 0) | |
| { | |
| // 遇到 ] 结束解析 | |
| if (tokens.Peek().type == JSONTokenType.ArrayEnd) | |
| break; | |
| // 处理2个值之间的逗号分隔 | |
| if (hasMoreValues) | |
| { | |
| if (tokens.Peek().type == JSONTokenType.Comma) | |
| tokens.Dequeue(); // 消费逗号 | |
| else | |
| return new JSONParseErrorStack(JSONParseError.MissingComma); | |
| } | |
| var error = this.ParserValue(tokens, out JSONElement item); | |
| if (item != null) | |
| typedResult.array.Add(item); | |
| if (error != null) | |
| { | |
| error.stack.Push(JSONParseError.ArrayParseError); | |
| return error; | |
| } | |
| hasMoreValues = true; | |
| } | |
| if (tokens.Count > 0 && tokens.Peek().type == JSONTokenType.ArrayEnd) | |
| tokens.Dequeue(); | |
| else | |
| return new JSONParseErrorStack(JSONParseError.ArrayParseError); | |
| return null; | |
| } | |
| private JSONParseErrorStack ParserKeyValuePair(Queue<JSONToken> tokens, out string key, out JSONElement value) | |
| { | |
| key = null; | |
| value = null; | |
| if (tokens.Count == 0) | |
| return new JSONParseErrorStack(JSONParseError.InvalidKeyValuePair); | |
| // 解析字符串键 | |
| if (tokens.Peek().type == JSONTokenType.String) | |
| key = tokens.Dequeue().value; | |
| else | |
| return new JSONParseErrorStack(JSONParseError.InvalidKeyValuePair); | |
| // 检查冒号 | |
| if (tokens.Count > 0 && tokens.Peek().type == JSONTokenType.Colon) | |
| tokens.Dequeue(); | |
| else | |
| return new JSONParseErrorStack(JSONParseError.InvalidKeyValuePair); | |
| var error = this.ParserValue(tokens, out value); | |
| if (error != null) | |
| { | |
| error.stack.Push(JSONParseError.InvalidKeyValuePair); | |
| return error; | |
| } | |
| return null; | |
| } | |
| private JSONParseErrorStack ParserValue(Queue<JSONToken> tokens, out JSONElement result) | |
| { | |
| result = new JSONElement(); | |
| if (tokens.Count == 0) | |
| return new JSONParseErrorStack(JSONParseError.InvalidValue); | |
| var peek = tokens.Peek(); | |
| // 基础值 | |
| if (peek.type == JSONTokenType.String) | |
| { | |
| var literal = tokens.Dequeue().value; | |
| result = new JSONElement(literal); | |
| } | |
| else if (peek.type == JSONTokenType.BoolAndNumber) | |
| { | |
| var literal = tokens.Dequeue().value; | |
| if (literal == "null" || literal == "nil") | |
| result = new JSONElement(); | |
| else if (bool.TryParse(literal, out var boolval)) | |
| result = new JSONElement(boolval); | |
| else if (float.TryParse(literal, out var number)) | |
| result = new JSONElement(number); | |
| else | |
| { | |
| var msg = "Invalid literal value: " + literal; | |
| return new JSONParseErrorStack(JSONParseError.InvalidValue, msg); | |
| } | |
| } | |
| // 复合值 | |
| else if (peek.type == JSONTokenType.ObjectBegin) | |
| { | |
| return this.ParserObject(tokens, out result); | |
| } | |
| else if (peek.type == JSONTokenType.ArrayBegin) | |
| { | |
| return this.ParserArray(tokens, out result); | |
| } | |
| // 无效值 | |
| else | |
| { | |
| var msg = "Invalid value type: " + peek; | |
| return new JSONParseErrorStack(JSONParseError.InvalidValue, msg); | |
| } | |
| return null; | |
| } | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment