// Written in the D programming language. /** JavaScript Object Notation Copyright: Copyright Jeremie Pelletier 2008 - 2009. License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). Authors: Jeremie Pelletier, David Herberth References: $(LINK http://json.org/) Source: $(PHOBOSSRC std/_json.d) */ /* Copyright Jeremie Pelletier 2008 - 2009. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) */ module std.json; import std.array; import std.conv; import std.range.primitives; import std.traits; /// @system unittest { import std.conv : to; // parse a file or string of json into a usable structure string s = `{ "language": "D", "rating": 3.5, "code": "42" }`; JSONValue j = parseJSON(s); // j and j["language"] return JSONValue, // j["language"].str returns a string assert(j["language"].str == "D"); assert(j["rating"].floating == 3.5); // check a type long x; if (const(JSONValue)* code = "code" in j) { if (code.type() == JSON_TYPE.INTEGER) x = code.integer; else x = to!int(code.str); } // create a json struct JSONValue jj = [ "language": "D" ]; // rating doesnt exist yet, so use .object to assign jj.object["rating"] = JSONValue(3.5); // create an array to assign to list jj.object["list"] = JSONValue( ["a", "b", "c"] ); // list already exists, so .object optional jj["list"].array ~= JSONValue("D"); string jjStr = `{"language":"D","list":["a","b","c","D"],"rating":3.5}`; assert(jj.toString == jjStr); } /** String literals used to represent special float values within JSON strings. */ enum JSONFloatLiteral : string { nan = "NaN", /// string representation of floating-point NaN inf = "Infinite", /// string representation of floating-point Infinity negativeInf = "-Infinite", /// string representation of floating-point negative Infinity } /** Flags that control how json is encoded and parsed. */ enum JSONOptions { none, /// standard parsing specialFloatLiterals = 0x1, /// encode NaN and Inf float values as strings escapeNonAsciiChars = 0x2, /// encode non ascii characters with an unicode escape sequence doNotEscapeSlashes = 0x4, /// do not escape slashes ('/') } /** JSON type enumeration */ enum JSON_TYPE : byte { /// Indicates the type of a $(D JSONValue). NULL, STRING, /// ditto INTEGER, /// ditto UINTEGER,/// ditto FLOAT, /// ditto OBJECT, /// ditto ARRAY, /// ditto TRUE, /// ditto FALSE /// ditto } /** JSON value node */ struct JSONValue { import std.exception : enforceEx, enforce; union Store { string str; long integer; ulong uinteger; double floating; JSONValue[string] object; JSONValue[] array; } private Store store; private JSON_TYPE type_tag; /** Returns the JSON_TYPE of the value stored in this structure. */ @property JSON_TYPE type() const pure nothrow @safe @nogc { return type_tag; } /// @safe unittest { string s = "{ \"language\": \"D\" }"; JSONValue j = parseJSON(s); assert(j.type == JSON_TYPE.OBJECT); assert(j["language"].type == JSON_TYPE.STRING); } /*** * Value getter/setter for $(D JSON_TYPE.STRING). * Throws: $(D JSONException) for read access if $(D type) is not * $(D JSON_TYPE.STRING). */ @property string str() const pure @trusted { enforce!JSONException(type == JSON_TYPE.STRING, "JSONValue is not a string"); return store.str; } /// ditto @property string str(string v) pure nothrow @nogc @safe { assign(v); return v; } /// @safe unittest { JSONValue j = [ "language": "D" ]; // get value assert(j["language"].str == "D"); // change existing key to new string j["language"].str = "Perl"; assert(j["language"].str == "Perl"); } /*** * Value getter/setter for $(D JSON_TYPE.INTEGER). * Throws: $(D JSONException) for read access if $(D type) is not * $(D JSON_TYPE.INTEGER). */ @property inout(long) integer() inout pure @safe { enforce!JSONException(type == JSON_TYPE.INTEGER, "JSONValue is not an integer"); return store.integer; } /// ditto @property long integer(long v) pure nothrow @safe @nogc { assign(v); return store.integer; } /*** * Value getter/setter for $(D JSON_TYPE.UINTEGER). * Throws: $(D JSONException) for read access if $(D type) is not * $(D JSON_TYPE.UINTEGER). */ @property inout(ulong) uinteger() inout pure @safe { enforce!JSONException(type == JSON_TYPE.UINTEGER, "JSONValue is not an unsigned integer"); return store.uinteger; } /// ditto @property ulong uinteger(ulong v) pure nothrow @safe @nogc { assign(v); return store.uinteger; } /*** * Value getter/setter for $(D JSON_TYPE.FLOAT). Note that despite * the name, this is a $(B 64)-bit `double`, not a 32-bit `float`. * Throws: $(D JSONException) for read access if $(D type) is not * $(D JSON_TYPE.FLOAT). */ @property inout(double) floating() inout pure @safe { enforce!JSONException(type == JSON_TYPE.FLOAT, "JSONValue is not a floating type"); return store.floating; } /// ditto @property double floating(double v) pure nothrow @safe @nogc { assign(v); return store.floating; } /*** * Value getter/setter for $(D JSON_TYPE.OBJECT). * Throws: $(D JSONException) for read access if $(D type) is not * $(D JSON_TYPE.OBJECT). * Note: this is @system because of the following pattern: --- auto a = &(json.object()); json.uinteger = 0; // overwrite AA pointer (*a)["hello"] = "world"; // segmentation fault --- */ @property ref inout(JSONValue[string]) object() inout pure @system { enforce!JSONException(type == JSON_TYPE.OBJECT, "JSONValue is not an object"); return store.object; } /// ditto @property JSONValue[string] object(JSONValue[string] v) pure nothrow @nogc @safe { assign(v); return v; } /*** * Value getter for $(D JSON_TYPE.OBJECT). * Unlike $(D object), this retrieves the object by value and can be used in @safe code. * * A caveat is that, if the returned value is null, modifications will not be visible: * --- * JSONValue json; * json.object = null; * json.objectNoRef["hello"] = JSONValue("world"); * assert("hello" !in json.object); * --- * * Throws: $(D JSONException) for read access if $(D type) is not * $(D JSON_TYPE.OBJECT). */ @property inout(JSONValue[string]) objectNoRef() inout pure @trusted { enforce!JSONException(type == JSON_TYPE.OBJECT, "JSONValue is not an object"); return store.object; } /*** * Value getter/setter for $(D JSON_TYPE.ARRAY). * Throws: $(D JSONException) for read access if $(D type) is not * $(D JSON_TYPE.ARRAY). * Note: this is @system because of the following pattern: --- auto a = &(json.array()); json.uinteger = 0; // overwrite array pointer (*a)[0] = "world"; // segmentation fault --- */ @property ref inout(JSONValue[]) array() inout pure @system { enforce!JSONException(type == JSON_TYPE.ARRAY, "JSONValue is not an array"); return store.array; } /// ditto @property JSONValue[] array(JSONValue[] v) pure nothrow @nogc @safe { assign(v); return v; } /*** * Value getter for $(D JSON_TYPE.ARRAY). * Unlike $(D array), this retrieves the array by value and can be used in @safe code. * * A caveat is that, if you append to the returned array, the new values aren't visible in the * JSONValue: * --- * JSONValue json; * json.array = [JSONValue("hello")]; * json.arrayNoRef ~= JSONValue("world"); * assert(json.array.length == 1); * --- * * Throws: $(D JSONException) for read access if $(D type) is not * $(D JSON_TYPE.ARRAY). */ @property inout(JSONValue[]) arrayNoRef() inout pure @trusted { enforce!JSONException(type == JSON_TYPE.ARRAY, "JSONValue is not an array"); return store.array; } /// Test whether the type is $(D JSON_TYPE.NULL) @property bool isNull() const pure nothrow @safe @nogc { return type == JSON_TYPE.NULL; } private void assign(T)(T arg) @safe { static if (is(T : typeof(null))) { type_tag = JSON_TYPE.NULL; } else static if (is(T : string)) { type_tag = JSON_TYPE.STRING; string t = arg; () @trusted { store.str = t; }(); } else static if (isSomeString!T) // issue 15884 { type_tag = JSON_TYPE.STRING; // FIXME: std.array.array(Range) is not deduced as 'pure' () @trusted { import std.utf : byUTF; store.str = cast(immutable)(arg.byUTF!char.array); }(); } else static if (is(T : bool)) { type_tag = arg ? JSON_TYPE.TRUE : JSON_TYPE.FALSE; } else static if (is(T : ulong) && isUnsigned!T) { type_tag = JSON_TYPE.UINTEGER; store.uinteger = arg; } else static if (is(T : long)) { type_tag = JSON_TYPE.INTEGER; store.integer = arg; } else static if (isFloatingPoint!T) { type_tag = JSON_TYPE.FLOAT; store.floating = arg; } else static if (is(T : Value[Key], Key, Value)) { static assert(is(Key : string), "AA key must be string"); type_tag = JSON_TYPE.OBJECT; static if (is(Value : JSONValue)) { JSONValue[string] t = arg; () @trusted { store.object = t; }(); } else { JSONValue[string] aa; foreach (key, value; arg) aa[key] = JSONValue(value); () @trusted { store.object = aa; }(); } } else static if (isArray!T) { type_tag = JSON_TYPE.ARRAY; static if (is(ElementEncodingType!T : JSONValue)) { JSONValue[] t = arg; () @trusted { store.array = t; }(); } else { JSONValue[] new_arg = new JSONValue[arg.length]; foreach (i, e; arg) new_arg[i] = JSONValue(e); () @trusted { store.array = new_arg; }(); } } else static if (is(T : JSONValue)) { type_tag = arg.type; store = arg.store; } else { static assert(false, text(`unable to convert type "`, T.stringof, `" to json`)); } } private void assignRef(T)(ref T arg) if (isStaticArray!T) { type_tag = JSON_TYPE.ARRAY; static if (is(ElementEncodingType!T : JSONValue)) { store.array = arg; } else { JSONValue[] new_arg = new JSONValue[arg.length]; foreach (i, e; arg) new_arg[i] = JSONValue(e); store.array = new_arg; } } /** * Constructor for $(D JSONValue). If $(D arg) is a $(D JSONValue) * its value and type will be copied to the new $(D JSONValue). * Note that this is a shallow copy: if type is $(D JSON_TYPE.OBJECT) * or $(D JSON_TYPE.ARRAY) then only the reference to the data will * be copied. * Otherwise, $(D arg) must be implicitly convertible to one of the * following types: $(D typeof(null)), $(D string), $(D ulong), * $(D long), $(D double), an associative array $(D V[K]) for any $(D V) * and $(D K) i.e. a JSON object, any array or $(D bool). The type will * be set accordingly. */ this(T)(T arg) if (!isStaticArray!T) { assign(arg); } /// Ditto this(T)(ref T arg) if (isStaticArray!T) { assignRef(arg); } /// Ditto this(T : JSONValue)(inout T arg) inout { store = arg.store; type_tag = arg.type; } /// @safe unittest { JSONValue j = JSONValue( "a string" ); j = JSONValue(42); j = JSONValue( [1, 2, 3] ); assert(j.type == JSON_TYPE.ARRAY); j = JSONValue( ["language": "D"] ); assert(j.type == JSON_TYPE.OBJECT); } void opAssign(T)(T arg) if (!isStaticArray!T && !is(T : JSONValue)) { assign(arg); } void opAssign(T)(ref T arg) if (isStaticArray!T) { assignRef(arg); } /*** * Array syntax for json arrays. * Throws: $(D JSONException) if $(D type) is not $(D JSON_TYPE.ARRAY). */ ref inout(JSONValue) opIndex(size_t i) inout pure @safe { auto a = this.arrayNoRef; enforceEx!JSONException(i < a.length, "JSONValue array index is out of range"); return a[i]; } /// @safe unittest { JSONValue j = JSONValue( [42, 43, 44] ); assert( j[0].integer == 42 ); assert( j[1].integer == 43 ); } /*** * Hash syntax for json objects. * Throws: $(D JSONException) if $(D type) is not $(D JSON_TYPE.OBJECT). */ ref inout(JSONValue) opIndex(string k) inout pure @safe { auto o = this.objectNoRef; return *enforce!JSONException(k in o, "Key not found: " ~ k); } /// @safe unittest { JSONValue j = JSONValue( ["language": "D"] ); assert( j["language"].str == "D" ); } /*** * Operator sets $(D value) for element of JSON object by $(D key). * * If JSON value is null, then operator initializes it with object and then * sets $(D value) for it. * * Throws: $(D JSONException) if $(D type) is not $(D JSON_TYPE.OBJECT) * or $(D JSON_TYPE.NULL). */ void opIndexAssign(T)(auto ref T value, string key) pure { enforceEx!JSONException(type == JSON_TYPE.OBJECT || type == JSON_TYPE.NULL, "JSONValue must be object or null"); JSONValue[string] aa = null; if (type == JSON_TYPE.OBJECT) { aa = this.objectNoRef; } aa[key] = value; this.object = aa; } /// @safe unittest { JSONValue j = JSONValue( ["language": "D"] ); j["language"].str = "Perl"; assert( j["language"].str == "Perl" ); } void opIndexAssign(T)(T arg, size_t i) pure { auto a = this.arrayNoRef; enforceEx!JSONException(i < a.length, "JSONValue array index is out of range"); a[i] = arg; this.array = a; } /// @safe unittest { JSONValue j = JSONValue( ["Perl", "C"] ); j[1].str = "D"; assert( j[1].str == "D" ); } JSONValue opBinary(string op : "~", T)(T arg) @safe { auto a = this.arrayNoRef; static if (isArray!T) { return JSONValue(a ~ JSONValue(arg).arrayNoRef); } else static if (is(T : JSONValue)) { return JSONValue(a ~ arg.arrayNoRef); } else { static assert(false, "argument is not an array or a JSONValue array"); } } void opOpAssign(string op : "~", T)(T arg) @safe { auto a = this.arrayNoRef; static if (isArray!T) { a ~= JSONValue(arg).arrayNoRef; } else static if (is(T : JSONValue)) { a ~= arg.arrayNoRef; } else { static assert(false, "argument is not an array or a JSONValue array"); } this.array = a; } /** * Support for the $(D in) operator. * * Tests wether a key can be found in an object. * * Returns: * when found, the $(D const(JSONValue)*) that matches to the key, * otherwise $(D null). * * Throws: $(D JSONException) if the right hand side argument $(D JSON_TYPE) * is not $(D OBJECT). */ auto opBinaryRight(string op : "in")(string k) const @safe { return k in this.objectNoRef; } /// @safe unittest { JSONValue j = [ "language": "D", "author": "walter" ]; string a = ("author" in j).str; } bool opEquals(const JSONValue rhs) const @nogc nothrow pure @safe { return opEquals(rhs); } bool opEquals(ref const JSONValue rhs) const @nogc nothrow pure @trusted { // Default doesn't work well since store is a union. Compare only // what should be in store. // This is @trusted to remain nogc, nothrow, fast, and usable from @safe code. if (type_tag != rhs.type_tag) return false; final switch (type_tag) { case JSON_TYPE.STRING: return store.str == rhs.store.str; case JSON_TYPE.INTEGER: return store.integer == rhs.store.integer; case JSON_TYPE.UINTEGER: return store.uinteger == rhs.store.uinteger; case JSON_TYPE.FLOAT: return store.floating == rhs.store.floating; case JSON_TYPE.OBJECT: return store.object == rhs.store.object; case JSON_TYPE.ARRAY: return store.array == rhs.store.array; case JSON_TYPE.TRUE: case JSON_TYPE.FALSE: case JSON_TYPE.NULL: return true; } } /// Implements the foreach $(D opApply) interface for json arrays. int opApply(scope int delegate(size_t index, ref JSONValue) dg) @system { int result; foreach (size_t index, ref value; array) { result = dg(index, value); if (result) break; } return result; } /// Implements the foreach $(D opApply) interface for json objects. int opApply(scope int delegate(string key, ref JSONValue) dg) @system { enforce!JSONException(type == JSON_TYPE.OBJECT, "JSONValue is not an object"); int result; foreach (string key, ref value; object) { result = dg(key, value); if (result) break; } return result; } /*** * Implicitly calls $(D toJSON) on this JSONValue. * * $(I options) can be used to tweak the conversion behavior. */ string toString(in JSONOptions options = JSONOptions.none) const @safe { return toJSON(this, false, options); } /*** * Implicitly calls $(D toJSON) on this JSONValue, like $(D toString), but * also passes $(I true) as $(I pretty) argument. * * $(I options) can be used to tweak the conversion behavior */ string toPrettyString(in JSONOptions options = JSONOptions.none) const @safe { return toJSON(this, true, options); } } /** Parses a serialized string and returns a tree of JSON values. Throws: $(LREF JSONException) if the depth exceeds the max depth. Params: json = json-formatted string to parse maxDepth = maximum depth of nesting allowed, -1 disables depth checking options = enable decoding string representations of NaN/Inf as float values */ JSONValue parseJSON(T)(T json, int maxDepth = -1, JSONOptions options = JSONOptions.none) if (isInputRange!T && !isInfinite!T && isSomeChar!(ElementEncodingType!T)) { import std.ascii : isWhite, isDigit, isHexDigit, toUpper, toLower; import std.typecons : Yes; JSONValue root; root.type_tag = JSON_TYPE.NULL; // Avoid UTF decoding when possible, as it is unnecessary when // processing JSON. static if (is(T : const(char)[])) alias Char = char; else alias Char = Unqual!(ElementType!T); if (json.empty) return root; int depth = -1; Char next = 0; int line = 1, pos = 0; void error(string msg) { throw new JSONException(msg, line, pos); } Char popChar() { if (json.empty) error("Unexpected end of data."); static if (is(T : const(char)[])) { Char c = json[0]; json = json[1..$]; } else { Char c = json.front; json.popFront(); } if (c == '\n') { line++; pos = 0; } else { pos++; } return c; } Char peekChar() { if (!next) { if (json.empty) return '\0'; next = popChar(); } return next; } void skipWhitespace() { while (isWhite(peekChar())) next = 0; } Char getChar(bool SkipWhitespace = false)() { static if (SkipWhitespace) skipWhitespace(); Char c; if (next) { c = next; next = 0; } else c = popChar(); return c; } void checkChar(bool SkipWhitespace = true, bool CaseSensitive = true)(char c) { static if (SkipWhitespace) skipWhitespace(); auto c2 = getChar(); static if (!CaseSensitive) c2 = toLower(c2); if (c2 != c) error(text("Found '", c2, "' when expecting '", c, "'.")); } bool testChar(bool SkipWhitespace = true, bool CaseSensitive = true)(char c) { static if (SkipWhitespace) skipWhitespace(); auto c2 = peekChar(); static if (!CaseSensitive) c2 = toLower(c2); if (c2 != c) return false; getChar(); return true; } wchar parseWChar() { wchar val = 0; foreach_reverse (i; 0 .. 4) { auto hex = toUpper(getChar()); if (!isHexDigit(hex)) error("Expecting hex character"); val += (isDigit(hex) ? hex - '0' : hex - ('A' - 10)) << (4 * i); } return val; } string parseString() { import std.ascii : isControl; import std.uni : isSurrogateHi, isSurrogateLo; import std.utf : encode, decode; auto str = appender!string(); Next: switch (peekChar()) { case '"': getChar(); break; case '\\': getChar(); auto c = getChar(); switch (c) { case '"': str.put('"'); break; case '\\': str.put('\\'); break; case '/': str.put('/'); break; case 'b': str.put('\b'); break; case 'f': str.put('\f'); break; case 'n': str.put('\n'); break; case 'r': str.put('\r'); break; case 't': str.put('\t'); break; case 'u': wchar wc = parseWChar(); dchar val; // Non-BMP characters are escaped as a pair of // UTF-16 surrogate characters (see RFC 4627). if (isSurrogateHi(wc)) { wchar[2] pair; pair[0] = wc; if (getChar() != '\\') error("Expected escaped low surrogate after escaped high surrogate"); if (getChar() != 'u') error("Expected escaped low surrogate after escaped high surrogate"); pair[1] = parseWChar(); size_t index = 0; val = decode(pair[], index); if (index != 2) error("Invalid escaped surrogate pair"); } else if (isSurrogateLo(wc)) error(text("Unexpected low surrogate")); else val = wc; char[4] buf; immutable len = encode!(Yes.useReplacementDchar)(buf, val); str.put(buf[0 .. len]); break; default: error(text("Invalid escape sequence '\\", c, "'.")); } goto Next; default: // RFC 7159 states that control characters U+0000 through // U+001F must not appear unescaped in a JSON string. auto c = getChar(); if (isControl(c)) error("Illegal control character."); str.put(c); goto Next; } return str.data.length ? str.data : ""; } bool tryGetSpecialFloat(string str, out double val) { switch (str) { case JSONFloatLiteral.nan: val = double.nan; return true; case JSONFloatLiteral.inf: val = double.infinity; return true; case JSONFloatLiteral.negativeInf: val = -double.infinity; return true; default: return false; } } void parseValue(ref JSONValue value) { depth++; if (maxDepth != -1 && depth > maxDepth) error("Nesting too deep."); auto c = getChar!true(); switch (c) { case '{': if (testChar('}')) { value.object = null; break; } JSONValue[string] obj; do { checkChar('"'); string name = parseString(); checkChar(':'); JSONValue member; parseValue(member); obj[name] = member; } while (testChar(',')); value.object = obj; checkChar('}'); break; case '[': if (testChar(']')) { value.type_tag = JSON_TYPE.ARRAY; break; } JSONValue[] arr; do { JSONValue element; parseValue(element); arr ~= element; } while (testChar(',')); checkChar(']'); value.array = arr; break; case '"': auto str = parseString(); // if special float parsing is enabled, check if string represents NaN/Inf if ((options & JSONOptions.specialFloatLiterals) && tryGetSpecialFloat(str, value.store.floating)) { // found a special float, its value was placed in value.store.floating value.type_tag = JSON_TYPE.FLOAT; break; } value.type_tag = JSON_TYPE.STRING; value.store.str = str; break; case '0': .. case '9': case '-': auto number = appender!string(); bool isFloat, isNegative; void readInteger() { if (!isDigit(c)) error("Digit expected"); Next: number.put(c); if (isDigit(peekChar())) { c = getChar(); goto Next; } } if (c == '-') { number.put('-'); c = getChar(); isNegative = true; } readInteger(); if (testChar('.')) { isFloat = true; number.put('.'); c = getChar(); readInteger(); } if (testChar!(false, false)('e')) { isFloat = true; number.put('e'); if (testChar('+')) number.put('+'); else if (testChar('-')) number.put('-'); c = getChar(); readInteger(); } string data = number.data; if (isFloat) { value.type_tag = JSON_TYPE.FLOAT; value.store.floating = parse!double(data); } else { if (isNegative) value.store.integer = parse!long(data); else value.store.uinteger = parse!ulong(data); value.type_tag = !isNegative && value.store.uinteger & (1UL << 63) ? JSON_TYPE.UINTEGER : JSON_TYPE.INTEGER; } break; case 't': case 'T': value.type_tag = JSON_TYPE.TRUE; checkChar!(false, false)('r'); checkChar!(false, false)('u'); checkChar!(false, false)('e'); break; case 'f': case 'F': value.type_tag = JSON_TYPE.FALSE; checkChar!(false, false)('a'); checkChar!(false, false)('l'); checkChar!(false, false)('s'); checkChar!(false, false)('e'); break; case 'n': case 'N': value.type_tag = JSON_TYPE.NULL; checkChar!(false, false)('u'); checkChar!(false, false)('l'); checkChar!(false, false)('l'); break; default: error(text("Unexpected character '", c, "'.")); } depth--; } parseValue(root); return root; } @safe unittest { enum issue15742objectOfObject = `{ "key1": { "key2": 1 }}`; static assert(parseJSON(issue15742objectOfObject).type == JSON_TYPE.OBJECT); enum issue15742arrayOfArray = `[[1]]`; static assert(parseJSON(issue15742arrayOfArray).type == JSON_TYPE.ARRAY); } @safe unittest { // Ensure we can parse and use JSON from @safe code auto a = `{ "key1": { "key2": 1 }}`.parseJSON; assert(a["key1"]["key2"].integer == 1); assert(a.toString == `{"key1":{"key2":1}}`); } @system unittest { // Ensure we can parse JSON from a @system range. struct Range { string s; size_t index; @system { bool empty() { return index >= s.length; } void popFront() { index++; } char front() { return s[index]; } } } auto s = Range(`{ "key1": { "key2": 1 }}`); auto json = parseJSON(s); assert(json["key1"]["key2"].integer == 1); } /** Parses a serialized string and returns a tree of JSON values. Throws: $(REF JSONException, std,json) if the depth exceeds the max depth. Params: json = json-formatted string to parse options = enable decoding string representations of NaN/Inf as float values */ JSONValue parseJSON(T)(T json, JSONOptions options) if (isInputRange!T && !isInfinite!T && isSomeChar!(ElementEncodingType!T)) { return parseJSON!T(json, -1, options); } /** Takes a tree of JSON values and returns the serialized string. Any Object types will be serialized in a key-sorted order. If $(D pretty) is false no whitespaces are generated. If $(D pretty) is true serialized string is formatted to be human-readable. Set the $(LREF JSONOptions.specialFloatLiterals) flag is set in $(D options) to encode NaN/Infinity as strings. */ string toJSON(const ref JSONValue root, in bool pretty = false, in JSONOptions options = JSONOptions.none) @safe { auto json = appender!string(); void toStringImpl(Char)(string str) @safe { json.put('"'); foreach (Char c; str) { switch (c) { case '"': json.put("\\\""); break; case '\\': json.put("\\\\"); break; case '/': if (!(options & JSONOptions.doNotEscapeSlashes)) json.put('\\'); json.put('/'); break; case '\b': json.put("\\b"); break; case '\f': json.put("\\f"); break; case '\n': json.put("\\n"); break; case '\r': json.put("\\r"); break; case '\t': json.put("\\t"); break; default: { import std.ascii : isControl; import std.utf : encode; // Make sure we do UTF decoding iff we want to // escape Unicode characters. assert(((options & JSONOptions.escapeNonAsciiChars) != 0) == is(Char == dchar)); with (JSONOptions) if (isControl(c) || ((options & escapeNonAsciiChars) >= escapeNonAsciiChars && c >= 0x80)) { // Ensure non-BMP characters are encoded as a pair // of UTF-16 surrogate characters, as per RFC 4627. wchar[2] wchars; // 1 or 2 UTF-16 code units size_t wNum = encode(wchars, c); // number of UTF-16 code units foreach (wc; wchars[0 .. wNum]) { json.put("\\u"); foreach_reverse (i; 0 .. 4) { char ch = (wc >>> (4 * i)) & 0x0f; ch += ch < 10 ? '0' : 'A' - 10; json.put(ch); } } } else { json.put(c); } } } } json.put('"'); } void toString(string str) @safe { // Avoid UTF decoding when possible, as it is unnecessary when // processing JSON. if (options & JSONOptions.escapeNonAsciiChars) toStringImpl!dchar(str); else toStringImpl!char(str); } void toValue(ref in JSONValue value, ulong indentLevel) @safe { void putTabs(ulong additionalIndent = 0) { if (pretty) foreach (i; 0 .. indentLevel + additionalIndent) json.put(" "); } void putEOL() { if (pretty) json.put('\n'); } void putCharAndEOL(char ch) { json.put(ch); putEOL(); } final switch (value.type) { case JSON_TYPE.OBJECT: auto obj = value.objectNoRef; if (!obj.length) { json.put("{}"); } else { putCharAndEOL('{'); bool first = true; void emit(R)(R names) { foreach (name; names) { auto member = obj[name]; if (!first) putCharAndEOL(','); first = false; putTabs(1); toString(name); json.put(':'); if (pretty) json.put(' '); toValue(member, indentLevel + 1); } } import std.algorithm.sorting : sort; // @@@BUG@@@ 14439 // auto names = obj.keys; // aa.keys can't be called in @safe code auto names = new string[obj.length]; size_t i = 0; foreach (k, v; obj) { names[i] = k; i++; } sort(names); emit(names); putEOL(); putTabs(); json.put('}'); } break; case JSON_TYPE.ARRAY: auto arr = value.arrayNoRef; if (arr.empty) { json.put("[]"); } else { putCharAndEOL('['); foreach (i, el; arr) { if (i) putCharAndEOL(','); putTabs(1); toValue(el, indentLevel + 1); } putEOL(); putTabs(); json.put(']'); } break; case JSON_TYPE.STRING: toString(value.str); break; case JSON_TYPE.INTEGER: json.put(to!string(value.store.integer)); break; case JSON_TYPE.UINTEGER: json.put(to!string(value.store.uinteger)); break; case JSON_TYPE.FLOAT: import std.math : isNaN, isInfinity; auto val = value.store.floating; if (val.isNaN) { if (options & JSONOptions.specialFloatLiterals) { toString(JSONFloatLiteral.nan); } else { throw new JSONException( "Cannot encode NaN. Consider passing the specialFloatLiterals flag."); } } else if (val.isInfinity) { if (options & JSONOptions.specialFloatLiterals) { toString((val > 0) ? JSONFloatLiteral.inf : JSONFloatLiteral.negativeInf); } else { throw new JSONException( "Cannot encode Infinity. Consider passing the specialFloatLiterals flag."); } } else { import std.format : format; // The correct formula for the number of decimal digits needed for lossless round // trips is actually: // ceil(log(pow(2.0, double.mant_dig - 1)) / log(10.0) + 1) == (double.dig + 2) // Anything less will round off (1 + double.epsilon) json.put("%.18g".format(val)); } break; case JSON_TYPE.TRUE: json.put("true"); break; case JSON_TYPE.FALSE: json.put("false"); break; case JSON_TYPE.NULL: json.put("null"); break; } } toValue(root, 0); return json.data; } @safe unittest // bugzilla 12897 { JSONValue jv0 = JSONValue("test测试"); assert(toJSON(jv0, false, JSONOptions.escapeNonAsciiChars) == `"test\u6D4B\u8BD5"`); JSONValue jv00 = JSONValue("test\u6D4B\u8BD5"); assert(toJSON(jv00, false, JSONOptions.none) == `"test测试"`); assert(toJSON(jv0, false, JSONOptions.none) == `"test测试"`); JSONValue jv1 = JSONValue("été"); assert(toJSON(jv1, false, JSONOptions.escapeNonAsciiChars) == `"\u00E9t\u00E9"`); JSONValue jv11 = JSONValue("\u00E9t\u00E9"); assert(toJSON(jv11, false, JSONOptions.none) == `"été"`); assert(toJSON(jv1, false, JSONOptions.none) == `"été"`); } /** Exception thrown on JSON errors */ class JSONException : Exception { this(string msg, int line = 0, int pos = 0) pure nothrow @safe { if (line) super(text(msg, " (Line ", line, ":", pos, ")")); else super(msg); } this(string msg, string file, size_t line) pure nothrow @safe { super(msg, file, line); } } @system unittest { import std.exception; JSONValue jv = "123"; assert(jv.type == JSON_TYPE.STRING); assertNotThrown(jv.str); assertThrown!JSONException(jv.integer); assertThrown!JSONException(jv.uinteger); assertThrown!JSONException(jv.floating); assertThrown!JSONException(jv.object); assertThrown!JSONException(jv.array); assertThrown!JSONException(jv["aa"]); assertThrown!JSONException(jv[2]); jv = -3; assert(jv.type == JSON_TYPE.INTEGER); assertNotThrown(jv.integer); jv = cast(uint) 3; assert(jv.type == JSON_TYPE.UINTEGER); assertNotThrown(jv.uinteger); jv = 3.0; assert(jv.type == JSON_TYPE.FLOAT); assertNotThrown(jv.floating); jv = ["key" : "value"]; assert(jv.type == JSON_TYPE.OBJECT); assertNotThrown(jv.object); assertNotThrown(jv["key"]); assert("key" in jv); assert("notAnElement" !in jv); assertThrown!JSONException(jv["notAnElement"]); const cjv = jv; assert("key" in cjv); assertThrown!JSONException(cjv["notAnElement"]); foreach (string key, value; jv) { static assert(is(typeof(value) == JSONValue)); assert(key == "key"); assert(value.type == JSON_TYPE.STRING); assertNotThrown(value.str); assert(value.str == "value"); } jv = [3, 4, 5]; assert(jv.type == JSON_TYPE.ARRAY); assertNotThrown(jv.array); assertNotThrown(jv[2]); foreach (size_t index, value; jv) { static assert(is(typeof(value) == JSONValue)); assert(value.type == JSON_TYPE.INTEGER); assertNotThrown(value.integer); assert(index == (value.integer-3)); } jv = null; assert(jv.type == JSON_TYPE.NULL); assert(jv.isNull); jv = "foo"; assert(!jv.isNull); jv = JSONValue("value"); assert(jv.type == JSON_TYPE.STRING); assert(jv.str == "value"); JSONValue jv2 = JSONValue("value"); assert(jv2.type == JSON_TYPE.STRING); assert(jv2.str == "value"); JSONValue jv3 = JSONValue("\u001c"); assert(jv3.type == JSON_TYPE.STRING); assert(jv3.str == "\u001C"); } @system unittest { // Bugzilla 11504 JSONValue jv = 1; assert(jv.type == JSON_TYPE.INTEGER); jv.str = "123"; assert(jv.type == JSON_TYPE.STRING); assert(jv.str == "123"); jv.integer = 1; assert(jv.type == JSON_TYPE.INTEGER); assert(jv.integer == 1); jv.uinteger = 2u; assert(jv.type == JSON_TYPE.UINTEGER); assert(jv.uinteger == 2u); jv.floating = 1.5; assert(jv.type == JSON_TYPE.FLOAT); assert(jv.floating == 1.5); jv.object = ["key" : JSONValue("value")]; assert(jv.type == JSON_TYPE.OBJECT); assert(jv.object == ["key" : JSONValue("value")]); jv.array = [JSONValue(1), JSONValue(2), JSONValue(3)]; assert(jv.type == JSON_TYPE.ARRAY); assert(jv.array == [JSONValue(1), JSONValue(2), JSONValue(3)]); jv = true; assert(jv.type == JSON_TYPE.TRUE); jv = false; assert(jv.type == JSON_TYPE.FALSE); enum E{True = true} jv = E.True; assert(jv.type == JSON_TYPE.TRUE); } @system pure unittest { // Adding new json element via array() / object() directly JSONValue jarr = JSONValue([10]); foreach (i; 0 .. 9) jarr.array ~= JSONValue(i); assert(jarr.array.length == 10); JSONValue jobj = JSONValue(["key" : JSONValue("value")]); foreach (i; 0 .. 9) jobj.object[text("key", i)] = JSONValue(text("value", i)); assert(jobj.object.length == 10); } @system pure unittest { // Adding new json element without array() / object() access JSONValue jarr = JSONValue([10]); foreach (i; 0 .. 9) jarr ~= [JSONValue(i)]; assert(jarr.array.length == 10); JSONValue jobj = JSONValue(["key" : JSONValue("value")]); foreach (i; 0 .. 9) jobj[text("key", i)] = JSONValue(text("value", i)); assert(jobj.object.length == 10); // No array alias auto jarr2 = jarr ~ [1,2,3]; jarr2[0] = 999; assert(jarr[0] == JSONValue(10)); } @system unittest { // @system because JSONValue.array is @system import std.exception; // An overly simple test suite, if it can parse a serializated string and // then use the resulting values tree to generate an identical // serialization, both the decoder and encoder works. auto jsons = [ `null`, `true`, `false`, `0`, `123`, `-4321`, `0.25`, `-0.25`, `""`, `"hello\nworld"`, `"\"\\\/\b\f\n\r\t"`, `[]`, `[12,"foo",true,false]`, `{}`, `{"a":1,"b":null}`, `{"goodbye":[true,"or",false,["test",42,{"nested":{"a":23.5,"b":0.140625}}]],` ~`"hello":{"array":[12,null,{}],"json":"is great"}}`, ]; enum dbl1_844 = `1.8446744073709568`; version (MinGW) jsons ~= dbl1_844 ~ `e+019`; else jsons ~= dbl1_844 ~ `e+19`; JSONValue val; string result; foreach (json; jsons) { try { val = parseJSON(json); enum pretty = false; result = toJSON(val, pretty); assert(result == json, text(result, " should be ", json)); } catch (JSONException e) { import std.stdio : writefln; writefln(text(json, "\n", e.toString())); } } // Should be able to correctly interpret unicode entities val = parseJSON(`"\u003C\u003E"`); assert(toJSON(val) == "\"\<\>\""); assert(val.to!string() == "\"\<\>\""); val = parseJSON(`"\u0391\u0392\u0393"`); assert(toJSON(val) == "\"\Α\Β\Γ\""); assert(val.to!string() == "\"\Α\Β\Γ\""); val = parseJSON(`"\u2660\u2666"`); assert(toJSON(val) == "\"\♠\♦\""); assert(val.to!string() == "\"\♠\♦\""); //0x7F is a control character (see Unicode spec) val = parseJSON(`"\u007F"`); assert(toJSON(val) == "\"\\u007F\""); assert(val.to!string() == "\"\\u007F\""); with(parseJSON(`""`)) assert(str == "" && str !is null); with(parseJSON(`[]`)) assert(!array.length); // Formatting val = parseJSON(`{"a":[null,{"x":1},{},[]]}`); assert(toJSON(val, true) == `{ "a": [ null, { "x": 1 }, {}, [] ] }`); } @safe unittest { auto json = `"hello\nworld"`; const jv = parseJSON(json); assert(jv.toString == json); assert(jv.toPrettyString == json); } @system pure unittest { // Bugzilla 12969 JSONValue jv; jv["int"] = 123; assert(jv.type == JSON_TYPE.OBJECT); assert("int" in jv); assert(jv["int"].integer == 123); jv["array"] = [1, 2, 3, 4, 5]; assert(jv["array"].type == JSON_TYPE.ARRAY); assert(jv["array"][2].integer == 3); jv["str"] = "D language"; assert(jv["str"].type == JSON_TYPE.STRING); assert(jv["str"].str == "D language"); jv["bool"] = false; assert(jv["bool"].type == JSON_TYPE.FALSE); assert(jv.object.length == 4); jv = [5, 4, 3, 2, 1]; assert( jv.type == JSON_TYPE.ARRAY ); assert( jv[3].integer == 2 ); } @safe unittest { auto s = q"EOF [ 1, 2, 3, potato ] EOF"; import std.exception; auto e = collectException!JSONException(parseJSON(s)); assert(e.msg == "Unexpected character 'p'. (Line 5:3)", e.msg); } // handling of special float values (NaN, Inf, -Inf) @safe unittest { import std.exception : assertThrown; import std.math : isNaN, isInfinity; // expected representations of NaN and Inf enum { nanString = '"' ~ JSONFloatLiteral.nan ~ '"', infString = '"' ~ JSONFloatLiteral.inf ~ '"', negativeInfString = '"' ~ JSONFloatLiteral.negativeInf ~ '"', } // with the specialFloatLiterals option, encode NaN/Inf as strings assert(JSONValue(float.nan).toString(JSONOptions.specialFloatLiterals) == nanString); assert(JSONValue(double.infinity).toString(JSONOptions.specialFloatLiterals) == infString); assert(JSONValue(-real.infinity).toString(JSONOptions.specialFloatLiterals) == negativeInfString); // without the specialFloatLiterals option, throw on encoding NaN/Inf assertThrown!JSONException(JSONValue(float.nan).toString); assertThrown!JSONException(JSONValue(double.infinity).toString); assertThrown!JSONException(JSONValue(-real.infinity).toString); // when parsing json with specialFloatLiterals option, decode special strings as floats JSONValue jvNan = parseJSON(nanString, JSONOptions.specialFloatLiterals); JSONValue jvInf = parseJSON(infString, JSONOptions.specialFloatLiterals); JSONValue jvNegInf = parseJSON(negativeInfString, JSONOptions.specialFloatLiterals); assert(jvNan.floating.isNaN); assert(jvInf.floating.isInfinity && jvInf.floating > 0); assert(jvNegInf.floating.isInfinity && jvNegInf.floating < 0); // when parsing json without the specialFloatLiterals option, decode special strings as strings jvNan = parseJSON(nanString); jvInf = parseJSON(infString); jvNegInf = parseJSON(negativeInfString); assert(jvNan.str == JSONFloatLiteral.nan); assert(jvInf.str == JSONFloatLiteral.inf); assert(jvNegInf.str == JSONFloatLiteral.negativeInf); } pure nothrow @safe @nogc unittest { JSONValue testVal; testVal = "test"; testVal = 10; testVal = 10u; testVal = 1.0; testVal = (JSONValue[string]).init; testVal = JSONValue[].init; testVal = null; assert(testVal.isNull); } pure nothrow @safe unittest // issue 15884 { import std.typecons; void Test(C)() { C[] a = ['x']; JSONValue testVal = a; assert(testVal.type == JSON_TYPE.STRING); testVal = a.idup; assert(testVal.type == JSON_TYPE.STRING); } Test!char(); Test!wchar(); Test!dchar(); } @safe unittest // issue 15885 { enum bool realInDoublePrecision = real.mant_dig == double.mant_dig; static bool test(const double num0) { import std.math : feqrel; const json0 = JSONValue(num0); const num1 = to!double(toJSON(json0)); static if (realInDoublePrecision) return feqrel(num1, num0) >= (double.mant_dig - 1); else return num1 == num0; } assert(test( 0.23)); assert(test(-0.23)); assert(test(1.223e+24)); assert(test(23.4)); assert(test(0.0012)); assert(test(30738.22)); assert(test(1 + double.epsilon)); assert(test(double.min_normal)); static if (realInDoublePrecision) assert(test(-double.max / 2)); else assert(test(-double.max)); const minSub = double.min_normal * double.epsilon; assert(test(minSub)); assert(test(3*minSub)); } @safe unittest // issue 17555 { import std.exception : assertThrown; assertThrown!JSONException(parseJSON("\"a\nb\"")); } @safe unittest // issue 17556 { auto v = JSONValue("\U0001D11E"); auto j = toJSON(v, false, JSONOptions.escapeNonAsciiChars); assert(j == `"\uD834\uDD1E"`); } @safe unittest // issue 5904 { string s = `"\uD834\uDD1E"`; auto j = parseJSON(s); assert(j.str == "\U0001D11E"); } @safe unittest // issue 17557 { assert(parseJSON("\"\xFF\"").str == "\xFF"); assert(parseJSON("\"\U0001D11E\"").str == "\U0001D11E"); } @safe unittest // issue 17553 { auto v = JSONValue("\xFF"); assert(toJSON(v) == "\"\xFF\""); } @safe unittest { import std.utf; assert(parseJSON("\"\xFF\"".byChar).str == "\xFF"); assert(parseJSON("\"\U0001D11E\"".byChar).str == "\U0001D11E"); } @safe unittest // JSONOptions.doNotEscapeSlashes (issue 17587) { assert(parseJSON(`"/"`).toString == `"\/"`); assert(parseJSON(`"\/"`).toString == `"\/"`); assert(parseJSON(`"/"`).toString(JSONOptions.doNotEscapeSlashes) == `"/"`); assert(parseJSON(`"\/"`).toString(JSONOptions.doNotEscapeSlashes) == `"/"`); }