Skip to content

Instantly share code, notes, and snippets.

@yasar11732
Created July 29, 2019 18:50
Show Gist options
  • Save yasar11732/b7fbd001867e208bbb7b2ebfa45356b0 to your computer and use it in GitHub Desktop.
Save yasar11732/b7fbd001867e208bbb7b2ebfa45356b0 to your computer and use it in GitHub Desktop.
JSON parser and stringifier implemented in C# (Recursive Descent Parser)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace MiniWebFramework
{
public enum JSONValueType
{
NULL,
TRUE,
FALSE,
NUMBER,
STRING,
ARRAY,
OBJECT
}
public class JSONValue
{
public JSONValueType type { get; private set; }
public object value { get; private set; }
public JSONValue(JSONValueType t, object v)
{
type = t;
value = v;
}
}
public static class JSON
{
private static readonly char[] positive_digits = new char[] { '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private static readonly char[] all_digits = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private static readonly char[] white_space = new char[] { ' ', '\t', '\n', '\r' };
private static readonly char[] string_breakers = new char[] { '"', '\\', '\r', '\n', '\t', '\b', '\f'};
private static readonly char[] hexdigit = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f',
'A', 'B', 'C', 'D', 'E', 'F'
};
private class KVPair
{
public string key;
public JSONValue value;
}
private class ParseContext
{
private static readonly char EOT = '\x04';
private char[] data;
int position;
int cap;
public ParseContext(char[] _data)
{
data = _data;
cap = _data.Length;
position = 0;
}
public bool isEOT()
{
return !(position < cap);
}
public char Peek()
{
if(position < cap)
return data[position];
return EOT;
}
public char Read()
{
if (position < cap)
return data[position++];
return EOT;
}
public int Tell()
{
return position;
}
public void Seek(int p)
{
position = p;
}
public string StringFrom(int start)
{
int strlen = position - start;
char[] segment = new char[strlen];
for(var i = 0; i < strlen; i++)
{
segment[i] = data[start + i];
}
return new string(segment);
}
}
private static bool parse_literal(ParseContext p, string literal)
{
bool isMatch = true;
int current = p.Tell();
foreach(var c in literal)
{
if(c != p.Read())
{
isMatch = false;
break;
}
}
if (!isMatch)
p.Seek(current);
return isMatch;
}
private static bool parse_char(ParseContext p, char c)
{
if(c == p.Peek())
{
p.Read();
return true;
}
return false;
}
private static bool parse_any(ParseContext p, char[] options)
{
bool isMatch = false;
char next_char = p.Peek();
for(var i = 0; i < options.Length; i++)
{
if(options[i] == next_char)
{
isMatch = true;
p.Read();
break;
}
}
return isMatch;
}
private static bool parse_ws(ParseContext p)
{
int num_matches = 0;
while(parse_any(p, white_space))
{
num_matches++;
}
return num_matches > 0;
}
private static JSONValue parse_json_null(ParseContext p)
{
if (parse_literal(p, "null"))
return new JSONValue(JSONValueType.NULL, null);
return null;
}
private static JSONValue parse_json_true(ParseContext p)
{
if (parse_literal(p, "true"))
return new JSONValue(JSONValueType.TRUE, null);
return null;
}
private static JSONValue parse_json_false(ParseContext p)
{
if (parse_literal(p, "false"))
return new JSONValue(JSONValueType.FALSE, null);
return null;
}
private static JSONValue parse_json_number(ParseContext p)
{
int start = p.Tell();
parse_char(p, '-');
if(!parse_int(p))
{
p.Seek(start);
return null;
}
parse_frac(p);
parse_exponent(p);
return new JSONValue(JSONValueType.NUMBER, Convert.ToDouble(p.StringFrom(start)));
}
private static bool parse_exponent(ParseContext p)
{
int start = p.Tell();
if (!parse_any(p, new char[] { 'e', 'E' }))
return false;
parse_any(p, new char[] { '-', '+' });
if (!parse_any(p, all_digits))
{
p.Seek(start);
return false;
}
while(parse_any(p, all_digits))
{
}
return true;
}
private static bool parse_frac(ParseContext p)
{
int start = p.Tell();
if (!parse_char(p, '.'))
return false;
if (!parse_any(p, all_digits))
{
p.Seek(start);
return false;
}
while(parse_any(p, all_digits))
{
}
return true;
}
private static bool parse_int(ParseContext p)
{
if(parse_char(p, '0'))
{
return true;
}
if(!parse_any(p, positive_digits))
{
return false;
}
while(parse_any(p, all_digits))
{
}
return true;
}
private static string parse_string_token(ParseContext p)
{
int start = p.Tell();
if (!parse_char(p, '\"'))
return null;
while (parse_normal_char(p) || parse_escaped_char(p))
{
continue;
}
if(p.Peek() != '"')
{
p.Seek(start);
return null;
}
var result = p.StringFrom(start + 1); // +1 skip initial double quote
p.Read(); // consume ending double quote
return Regex.Unescape(result);
}
private static JSONValue parse_json_string(ParseContext p)
{
string s;
if ((s = parse_string_token(p)) != null)
return new JSONValue(JSONValueType.STRING, s);
else
return null;
}
private static bool parse_escaped_char(ParseContext p)
{
int start = p.Tell();
if (!parse_char(p, '\\'))
return false;
if (p.isEOT())
{
p.Seek(start);
return false;
}
var c = p.Read();
if (c != 'u')
return true;
if(
!parse_any(p, hexdigit) ||
!parse_any(p, hexdigit) ||
!parse_any(p, hexdigit) ||
!parse_any(p, hexdigit)
)
{
p.Seek(start);
return false;
}
return true;
}
private static bool parse_normal_char(ParseContext p)
{
var c = p.Peek();
if (!shouldescape(c))
{
p.Read();
return true;
} else
{
return false;
}
}
private static JSONValue parse_json_array(ParseContext p)
{
int start = p.Tell();
if (!parse_char(p, '['))
return null;
parse_ws(p); // skip optional space
List<JSONValue> elems = parse_elems(p);
if (!parse_char(p, ']'))
{
p.Seek(start);
return null;
}
return new JSONValue(JSONValueType.ARRAY, elems);
}
private static List<JSONValue> parse_elems(ParseContext p)
{
var elems = new List<JSONValue>();
var elem = parse_jsonvalue(p);
while(elem != null)
{
elems.Add(elem);
if (!parse_char(p, ','))
break;
elem = parse_jsonvalue(p);
}
return elems;
}
private static JSONValue parse_json_object(ParseContext p)
{
int start = p.Tell();
if (!parse_char(p, '{'))
return null;
parse_ws(p); // skip optional space
Dictionary<string, JSONValue> elems = parse_kvpairs(p);
if (!parse_char(p, '}'))
{
p.Seek(start);
return null;
}
return new JSONValue(JSONValueType.OBJECT, elems);
}
private static Dictionary<string, JSONValue> parse_kvpairs(ParseContext p)
{
KVPair pair;
Dictionary<string, JSONValue> pairs = new Dictionary<string, JSONValue>();
while((pair = parse_kvpair(p)) != null)
{
pairs[pair.key] = pair.value;
parse_ws(p); // skip space if any
if (!parse_char(p, ','))
break;
}
return pairs;
}
private static KVPair parse_kvpair(ParseContext p)
{
int start = p.Tell();
KVPair pair = new KVPair();
parse_ws(p); // skip white space
string s = parse_string_token(p);
if (s == null)
return null;
parse_ws(p); // skip whitespace
if(!parse_char(p, ':'))
{
p.Seek(start);
return null;
}
JSONValue v = parse_jsonvalue(p);
if(v == null)
{
p.Seek(start);
return null;
}
return new KVPair()
{
key = s,
value = v
};
}
private static JSONValue parse_jsonvalue(ParseContext p)
{
JSONValue v;
parse_ws(p);
if (
(v = parse_json_null(p) ) != null ||
(v = parse_json_true(p) ) != null ||
(v = parse_json_false(p) ) != null ||
(v = parse_json_number(p)) != null ||
(v = parse_json_string(p)) != null ||
(v = parse_json_array(p) ) != null ||
(v = parse_json_object(p)) != null
)
{
}
parse_ws(p);
return v;
}
public static JSONValue parse(string s)
{
return parse(s.ToCharArray());
}
public static JSONValue parse(char[] s)
{
ParseContext p = new ParseContext(s);
JSONValue v = parse_jsonvalue(p);
if (p.isEOT())
return v;
else
return null;
}
public static string stringify()
{
return "null";
}
public static string stringify(bool b)
{
if (b)
return "true";
else
return "false";
}
private static bool shouldescape(char c)
{
return !(c >= ' ' && c != '"' && c != '\\');
}
private static string escape(string s)
{
using (var ms = new MemoryStream())
using (var wr = new StreamWriter(ms))
{
foreach(char c in s)
{
if(shouldescape(c))
{
int x = c;
wr.Write("\\u");
wr.Write(x.ToString("X4"));
} else
{
wr.Write(c);
}
}
wr.Flush();
ms.Seek(0, SeekOrigin.Begin);
return Encoding.UTF8.GetString(ms.ToArray());
}
}
public static string stringify(string s)
{
return String.Format("\"{0}\"",escape(s));
}
public static string stringify(double d)
{
return d.ToString();
}
public static string stringify(List<object> l)
{
List<string> parts = new List<string>();
foreach(var o in l)
{
if (o == null)
{
parts.Add("null");
continue;
}
Type t = o.GetType();
do
{
if(t == typeof(bool))
{
if ((bool)o)
parts.Add("true");
else
parts.Add("false");
break;
}
if (t == typeof(string))
{
parts.Add(stringify((string)o));
break;
}
if (t == typeof(int) || t == typeof(double) || t == typeof(float))
{
parts.Add(stringify(Convert.ToDouble(o)));
break;
}
if (t == typeof(List<object>))
{
parts.Add(stringify((List<object>)o));
break;
}
if (t == typeof(Dictionary<string, object>))
{
parts.Add(stringify((Dictionary<string, object>)o));
break;
}
throw new InvalidDataException();
} while (false);
}
return "[" + String.Join(",", parts.ToArray()) + "]";
}
public static string stringify(Dictionary<string, object> o)
{
List<string> parts = new List<string>();
foreach(KeyValuePair<string, object> kv in o)
{
if (kv.Value == null)
{
parts.Add(String.Format("{0}:null", stringify(kv.Key)));
continue;
}
Type t = kv.Value.GetType();
do
{
if (t == typeof(bool))
{
if ((bool)kv.Value)
parts.Add(String.Format("{0}:true", stringify(kv.Key)));
else
parts.Add(String.Format("{0}:false", stringify(kv.Key)));
break;
}
if (t == typeof(string))
{
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify((string)kv.Value)));
break;
}
if (t == typeof(int) || t == typeof(double) || t == typeof(float))
{
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify(Convert.ToDouble(kv.Value))));
break;
}
if (t == typeof(List<object>))
{
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify((List<object>)kv.Value)));
break;
}
if (t == typeof(Dictionary<string, object>))
{
parts.Add(String.Format("{0}:{1}", stringify(kv.Key), stringify((Dictionary<string, object>)kv.Value)));
break;
}
throw new InvalidDataException();
} while (false);
}
return "{" + String.Join(",", parts.ToArray()) + "}";
}
}
}
@yasar11732
Copy link
Author

yasar11732 commented Jul 29, 2019

How To Use

  • You can create JSON strings using JSON.stringify() method.
  • You can parse JSON strings using JSON.parse() method.

JSON.parse() method returns values of type JSONValue. JSONValue has two readonly properties. JSONValue.type is of type enum JSONValueType. It is defined as;

public enum JSONValueType
    {
        NULL,
        TRUE,
        FALSE,
        NUMBER,
        STRING,
        ARRAY,
        OBJECT
    }

Other readonly property is JSONValue.value. This property should be interpreted according to type property. If type property is one of NULL,TRUE,FALSE, value property is not used. If type property is NUMBER, value can be converted to double. If type is STRING, value is string. If type is ARRAY, value is List<JSONValue>. Finally, if type is OBJECT, value is Dictionary<string, JSONValue>.

Parser and stringifier are standards-compliant as far as I can tell, but they are not tested throughly. Minor issues might exist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment