// Written in the D programming language. /** Implements functionality to read and write JavaScript Object Notation values. JavaScript Object Notation is a lightweight data interchange format commonly used in web services and configuration files. It's easy for humans to read and write, and it's easy for machines to parse and generate. $(RED Warning: While $(LREF JSONValue) is fine for small-scale use, at the range of hundreds of megabytes it is known to cause and exacerbate GC problems. If you encounter problems, try replacing it with a stream parser. See also $(LINK https://forum.dlang.org/post/dzfyaxypmkdrpakmycjv@forum.dlang.org).) 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/), $(LINK http://seriot.ch/parsing_json.html) 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; 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() == JSONType.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 and encoding specialFloatLiterals = 0x1, /// Encode NaN and Inf float values as strings escapeNonAsciiChars = 0x2, /// Encode non-ASCII characters with a Unicode escape sequence doNotEscapeSlashes = 0x4, /// Do not escape slashes ('/') strictParsing = 0x8, /// Strictly follow RFC-8259 grammar when parsing } /** Enumeration of JSON types */ enum JSONType : byte { /// Indicates the type of a `JSONValue`. null_, string, /// ditto integer, /// ditto uinteger, /// ditto float_, /// ditto array, /// ditto object, /// ditto true_, /// ditto false_, /// ditto // FIXME: Find some way to deprecate the enum members below, which does NOT // create lots of spam-like deprecation warnings, which can't be fixed // by the user. See discussion on this issue at // https://forum.dlang.org/post/feudrhtxkaxxscwhhhff@forum.dlang.org /* deprecated("Use .null_") */ NULL = null_, /* deprecated("Use .string") */ STRING = string, /* deprecated("Use .integer") */ INTEGER = integer, /* deprecated("Use .uinteger") */ UINTEGER = uinteger, /* deprecated("Use .float_") */ FLOAT = float_, /* deprecated("Use .array") */ ARRAY = array, /* deprecated("Use .object") */ OBJECT = object, /* deprecated("Use .true_") */ TRUE = true_, /* deprecated("Use .false_") */ FALSE = false_, } deprecated("Use JSONType and the new enum member names") alias JSON_TYPE = JSONType; /** JSON value node */ struct JSONValue { import std.exception : enforce; union Store { string str; long integer; ulong uinteger; double floating; JSONValue[string] object; JSONValue[] array; } private Store store; private JSONType type_tag; /** Returns the JSONType of the value stored in this structure. */ @property JSONType type() const pure nothrow @safe @nogc { return type_tag; } /// @safe unittest { string s = "{ \"language\": \"D\" }"; JSONValue j = parseJSON(s); assert(j.type == JSONType.object); assert(j["language"].type == JSONType.string); } /*** * Value getter/setter for `JSONType.string`. * Throws: `JSONException` for read access if `type` is not * `JSONType.string`. */ @property string str() const pure @trusted return scope { enforce!JSONException(type == JSONType.string, "JSONValue is not a string"); return store.str; } /// ditto @property string str(return scope string v) pure nothrow @nogc @trusted return // TODO make @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 `JSONType.integer`. * Throws: `JSONException` for read access if `type` is not * `JSONType.integer`. */ @property long integer() const pure @safe { enforce!JSONException(type == JSONType.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 `JSONType.uinteger`. * Throws: `JSONException` for read access if `type` is not * `JSONType.uinteger`. */ @property ulong uinteger() const pure @safe { enforce!JSONException(type == JSONType.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 `JSONType.float_`. Note that despite * the name, this is a $(B 64)-bit `double`, not a 32-bit `float`. * Throws: `JSONException` for read access if `type` is not * `JSONType.float_`. */ @property double floating() const pure @safe { enforce!JSONException(type == JSONType.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 boolean stored in JSON. * Throws: `JSONException` for read access if `this.type` is not * `JSONType.true_` or `JSONType.false_`. */ @property bool boolean() const pure @safe { if (type == JSONType.true_) return true; if (type == JSONType.false_) return false; throw new JSONException("JSONValue is not a boolean type"); } /// ditto @property bool boolean(bool v) pure nothrow @safe @nogc { assign(v); return v; } /// @safe unittest { JSONValue j = true; assert(j.boolean == true); j.boolean = false; assert(j.boolean == false); j.integer = 12; import std.exception : assertThrown; assertThrown!JSONException(j.boolean); } /*** * Value getter/setter for `JSONType.object`. * Throws: `JSONException` for read access if `type` is not * `JSONType.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 return { enforce!JSONException(type == JSONType.object, "JSONValue is not an object"); return store.object; } /// ditto @property JSONValue[string] object(return scope JSONValue[string] v) pure nothrow @nogc @trusted // TODO make @safe { assign(v); return v; } /*** * Value getter for `JSONType.object`. * Unlike `object`, this retrieves the object by value * and can be used in @safe code. * * One possible 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: `JSONException` for read access if `type` is not * `JSONType.object`. */ @property inout(JSONValue[string]) objectNoRef() inout pure @trusted { enforce!JSONException(type == JSONType.object, "JSONValue is not an object"); return store.object; } /*** * Value getter/setter for `JSONType.array`. * Throws: `JSONException` for read access if `type` is not * `JSONType.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() scope return inout pure @system { enforce!JSONException(type == JSONType.array, "JSONValue is not an array"); return store.array; } /// ditto @property JSONValue[] array(return scope JSONValue[] v) pure nothrow @nogc @trusted scope // TODO make @safe { assign(v); return v; } /*** * Value getter for `JSONType.array`. * Unlike `array`, this retrieves the array by value and can be used in @safe code. * * One possible 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: `JSONException` for read access if `type` is not * `JSONType.array`. */ @property inout(JSONValue[]) arrayNoRef() inout pure @trusted { enforce!JSONException(type == JSONType.array, "JSONValue is not an array"); return store.array; } /// Test whether the type is `JSONType.null_` @property bool isNull() const pure nothrow @safe @nogc { return type == JSONType.null_; } /*** * A convenience getter that returns this `JSONValue` as the specified D type. * Note: Only numeric types, `bool`, `string`, `JSONValue[string]`, and `JSONValue[]` types are accepted * Throws: `JSONException` if `T` cannot hold the contents of this `JSONValue` * `ConvException` in case of integer overflow when converting to `T` */ @property inout(T) get(T)() inout const pure @safe { static if (is(immutable T == immutable string)) { return str; } else static if (is(immutable T == immutable bool)) { return boolean; } else static if (isFloatingPoint!T) { switch (type) { case JSONType.float_: return cast(T) floating; case JSONType.uinteger: return cast(T) uinteger; case JSONType.integer: return cast(T) integer; default: throw new JSONException("JSONValue is not a number type"); } } else static if (isIntegral!T) { switch (type) { case JSONType.uinteger: return uinteger.to!T; case JSONType.integer: return integer.to!T; default: throw new JSONException("JSONValue is not a an integral type"); } } else { static assert(false, "Unsupported type"); } } // This specialization is needed because arrayNoRef requires inout @property inout(T) get(T : JSONValue[])() inout pure @trusted /// ditto { return arrayNoRef; } /// ditto @property inout(T) get(T : JSONValue[string])() inout pure @trusted { return object; } /// @safe unittest { import std.exception; import std.conv; string s = `{ "a": 123, "b": 3.1415, "c": "text", "d": true, "e": [1, 2, 3], "f": { "a": 1 }, "g": -45, "h": ` ~ ulong.max.to!string ~ `, }`; struct a { } immutable json = parseJSON(s); assert(json["a"].get!double == 123.0); assert(json["a"].get!int == 123); assert(json["a"].get!uint == 123); assert(json["b"].get!double == 3.1415); assertThrown!JSONException(json["b"].get!int); assert(json["c"].get!string == "text"); assert(json["d"].get!bool == true); assertNotThrown(json["e"].get!(JSONValue[])); assertNotThrown(json["f"].get!(JSONValue[string])); static assert(!__traits(compiles, json["a"].get!a)); assertThrown!JSONException(json["e"].get!float); assertThrown!JSONException(json["d"].get!(JSONValue[string])); assertThrown!JSONException(json["f"].get!(JSONValue[])); assert(json["g"].get!int == -45); assertThrown!ConvException(json["g"].get!uint); assert(json["h"].get!ulong == ulong.max); assertThrown!ConvException(json["h"].get!uint); assertNotThrown(json["h"].get!float); } private void assign(T)(T arg) { static if (is(T : typeof(null))) { type_tag = JSONType.null_; } else static if (is(T : string)) { type_tag = JSONType.string; string t = arg; () @trusted { store.str = t; }(); } // https://issues.dlang.org/show_bug.cgi?id=15884 else static if (isSomeString!T) { type_tag = JSONType.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 ? JSONType.true_ : JSONType.false_; } else static if (is(T : ulong) && isUnsigned!T) { type_tag = JSONType.uinteger; store.uinteger = arg; } else static if (is(T : long)) { type_tag = JSONType.integer; store.integer = arg; } else static if (isFloatingPoint!T) { type_tag = JSONType.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 = JSONType.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 = JSONType.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 = JSONType.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 `JSONValue`. If `arg` is a `JSONValue` * its value and type will be copied to the new `JSONValue`. * Note that this is a shallow copy: if type is `JSONType.object` * or `JSONType.array` then only the reference to the data will * be copied. * Otherwise, `arg` must be implicitly convertible to one of the * following types: `typeof(null)`, `string`, `ulong`, * `long`, `double`, an associative array `V[K]` for any `V` * and `K` i.e. a JSON object, any array or `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 == JSONType.array); j = JSONValue( ["language": "D"] ); assert(j.type == JSONType.object); } /** * An enum value that can be used to obtain a `JSONValue` representing * an empty JSON object. */ enum emptyObject = JSONValue(string[string].init); /// @system unittest { JSONValue obj1 = JSONValue.emptyObject; assert(obj1.type == JSONType.object); obj1.object["a"] = JSONValue(1); assert(obj1.object["a"] == JSONValue(1)); JSONValue obj2 = JSONValue.emptyObject; assert("a" !in obj2.object); obj2.object["b"] = JSONValue(5); assert(obj1 != obj2); } /** * An enum value that can be used to obtain a `JSONValue` representing * an empty JSON array. */ enum emptyArray = JSONValue(JSONValue[].init); /// @system unittest { JSONValue arr1 = JSONValue.emptyArray; assert(arr1.type == JSONType.array); assert(arr1.array.length == 0); arr1.array ~= JSONValue("Hello"); assert(arr1.array.length == 1); assert(arr1.array[0] == JSONValue("Hello")); JSONValue arr2 = JSONValue.emptyArray; assert(arr2.array.length == 0); assert(arr1 != arr2); } 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: `JSONException` if `type` is not `JSONType.array`. */ ref inout(JSONValue) opIndex(size_t i) inout pure @safe { auto a = this.arrayNoRef; enforce!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: `JSONException` if `type` is not `JSONType.object`. */ ref inout(JSONValue) opIndex(return scope 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" ); } /*** * Provides support for index assignments, which sets the * corresponding value of the JSON object's `key` field to `value`. * * If the `JSONValue` is `JSONType.null_`, then this function * initializes it with a JSON object and then performs * the index assignment. * * Throws: `JSONException` if `type` is not `JSONType.object` * or `JSONType.null_`. */ void opIndexAssign(T)(auto ref T value, string key) { enforce!JSONException(type == JSONType.object || type == JSONType.null_, "JSONValue must be object or null"); JSONValue[string] aa = null; if (type == JSONType.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" ); } /// ditto void opIndexAssign(T)(T arg, size_t i) { auto a = this.arrayNoRef; enforce!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) { 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) { 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; } /** * Provides support for the `in` operator. * * Tests whether a key can be found in an object. * * Returns: * When found, the `inout(JSONValue)*` that matches to the key, * otherwise `null`. * * Throws: `JSONException` if the right hand side argument `JSONType` * is not `object`. */ inout(JSONValue)* opBinaryRight(string op : "in")(string k) inout @safe { return k in this.objectNoRef; } /// @safe unittest { JSONValue j = [ "language": "D", "author": "walter" ]; string a = ("author" in j).str; *("author" in j) = "Walter"; assert(j["author"].str == "Walter"); } /// bool opEquals(const JSONValue rhs) const @nogc nothrow pure @safe { return opEquals(rhs); } /// ditto 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. final switch (type_tag) { case JSONType.integer: switch (rhs.type_tag) { case JSONType.integer: return store.integer == rhs.store.integer; case JSONType.uinteger: return store.integer == rhs.store.uinteger; case JSONType.float_: return store.integer == rhs.store.floating; default: return false; } case JSONType.uinteger: switch (rhs.type_tag) { case JSONType.integer: return store.uinteger == rhs.store.integer; case JSONType.uinteger: return store.uinteger == rhs.store.uinteger; case JSONType.float_: return store.uinteger == rhs.store.floating; default: return false; } case JSONType.float_: switch (rhs.type_tag) { case JSONType.integer: return store.floating == rhs.store.integer; case JSONType.uinteger: return store.floating == rhs.store.uinteger; case JSONType.float_: return store.floating == rhs.store.floating; default: return false; } case JSONType.string: return type_tag == rhs.type_tag && store.str == rhs.store.str; case JSONType.object: return type_tag == rhs.type_tag && store.object == rhs.store.object; case JSONType.array: return type_tag == rhs.type_tag && store.array == rhs.store.array; case JSONType.true_: case JSONType.false_: case JSONType.null_: return type_tag == rhs.type_tag; } } /// @safe unittest { assert(JSONValue(0u) == JSONValue(0)); assert(JSONValue(0u) == JSONValue(0.0)); assert(JSONValue(0) == JSONValue(0.0)); } /// Implements the foreach `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 `opApply` interface for json objects. int opApply(scope int delegate(string key, ref JSONValue) dg) @system { enforce!JSONException(type == JSONType.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 `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); } /// void toString(Out)(Out sink, in JSONOptions options = JSONOptions.none) const { toJSON(sink, this, false, options); } /*** * Implicitly calls `toJSON` on this JSONValue, like `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); } /// void toPrettyString(Out)(Out sink, in JSONOptions options = JSONOptions.none) const { toJSON(sink, this, true, options); } } // https://issues.dlang.org/show_bug.cgi?id=20874 @system unittest { static struct MyCustomType { public string toString () const @system { return null; } alias toString this; } static struct B { public JSONValue asJSON() const @system { return JSONValue.init; } alias asJSON this; } if (false) // Just checking attributes { JSONValue json; MyCustomType ilovedlang; json = ilovedlang; json["foo"] = ilovedlang; auto s = ilovedlang in json; B b; json ~= b; json ~ b; } } /** Parses a serialized string and returns a tree of JSON values. Throws: $(LREF JSONException) if string does not follow the JSON grammar or the depth exceeds the max depth, $(LREF ConvException) if a number in the input cannot be represented by a native D type. 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 (isSomeFiniteCharInputRange!T) { import std.ascii : isDigit, isHexDigit, toUpper, toLower; import std.typecons : Nullable, Yes; JSONValue root; root.type_tag = JSONType.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); int depth = -1; Nullable!Char next; int line = 1, pos = 0; immutable bool strict = (options & JSONOptions.strictParsing) != 0; void error(string msg) { throw new JSONException(msg, line, pos); } if (json.empty) { if (strict) { error("Empty JSON body"); } return root; } bool isWhite(dchar c) { if (strict) { // RFC 7159 has a stricter definition of whitespace than general ASCII. return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } import std.ascii : isWhite; // Accept ASCII NUL as whitespace in non-strict mode. return c == 0 || isWhite(c); } 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.isNull) { if (json.empty) return '\0'; next = popChar(); } return next.get; } Nullable!Char peekCharNullable() { if (next.isNull && !json.empty) { next = popChar(); } return next; } void skipWhitespace() { while (true) { auto c = peekCharNullable(); if (c.isNull || !isWhite(c.get)) { return; } next.nullify(); } } Char getChar(bool SkipWhitespace = false)() { static if (SkipWhitespace) skipWhitespace(); Char c; if (!next.isNull) { c = next.get; next.nullify(); } else c = popChar(); return c; } void checkChar(bool SkipWhitespace = true)(char c, bool caseSensitive = true) { static if (SkipWhitespace) skipWhitespace(); auto c2 = getChar(); 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.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. // Note: std.ascii.isControl can't be used for this test // because it considers ASCII DEL (0x7f) to be a control // character but RFC 7159 does not. // Accept unescaped ASCII NULs in non-strict mode. auto c = getChar(); if (c < 0x20 && (strict || c != 0)) 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 { skipWhitespace(); if (!strict && peekChar() == '}') { break; } 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 = JSONType.array; break; } JSONValue[] arr; do { skipWhitespace(); if (!strict && peekChar() == ']') { break; } 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 = JSONType.float_; break; } value.assign(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; } if (strict && c == '0') { number.put('0'); if (isDigit(peekChar())) { error("Additional digits not allowed after initial zero digit"); } } else { 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 = JSONType.float_; value.store.floating = parse!double(data); } else { if (isNegative) { value.store.integer = parse!long(data); value.type_tag = JSONType.integer; } else { // only set the correct union member to not confuse CTFE ulong u = parse!ulong(data); if (u & (1UL << 63)) { value.store.uinteger = u; value.type_tag = JSONType.uinteger; } else { value.store.integer = u; value.type_tag = JSONType.integer; } } } break; case 'T': if (strict) goto default; goto case; case 't': value.type_tag = JSONType.true_; checkChar!false('r', strict); checkChar!false('u', strict); checkChar!false('e', strict); break; case 'F': if (strict) goto default; goto case; case 'f': value.type_tag = JSONType.false_; checkChar!false('a', strict); checkChar!false('l', strict); checkChar!false('s', strict); checkChar!false('e', strict); break; case 'N': if (strict) goto default; goto case; case 'n': value.type_tag = JSONType.null_; checkChar!false('u', strict); checkChar!false('l', strict); checkChar!false('l', strict); break; default: error(text("Unexpected character '", c, "'.")); } depth--; } parseValue(root); if (strict) { skipWhitespace(); if (!peekCharNullable().isNull) error("Trailing non-whitespace characters"); } return root; } @safe unittest { enum issue15742objectOfObject = `{ "key1": { "key2": 1 }}`; static assert(parseJSON(issue15742objectOfObject).type == JSONType.object); enum issue15742arrayOfArray = `[[1]]`; static assert(parseJSON(issue15742arrayOfArray).type == JSONType.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); } // https://issues.dlang.org/show_bug.cgi?id=20527 @safe unittest { static assert(parseJSON(`{"a" : 2}`)["a"].integer == 2); } /** 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 options = enable decoding string representations of NaN/Inf as float values */ JSONValue parseJSON(T)(T json, JSONOptions options) if (isSomeFiniteCharInputRange!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 `pretty` is false no whitespaces are generated. If `pretty` is true serialized string is formatted to be human-readable. Set the $(LREF JSONOptions.specialFloatLiterals) flag is set in `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(); toJSON(json, root, pretty, options); return json.data; } /// void toJSON(Out)( auto ref Out json, const ref JSONValue root, in bool pretty = false, in JSONOptions options = JSONOptions.none) if (isOutputRange!(Out,char)) { void toStringImpl(Char)(string str) { 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), "JSONOptions.escapeNonAsciiChars needs dchar strings"); 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) { // Avoid UTF decoding when possible, as it is unnecessary when // processing JSON. if (options & JSONOptions.escapeNonAsciiChars) toStringImpl!dchar(str); else toStringImpl!char(str); } /* make the function infer @system when json.put() is @system */ if (0) json.put(' '); /* Mark as @trusted because json.put() may be @system. This has difficulty * inferring @safe because it is recursive. */ void toValueImpl(ref const JSONValue value, ulong indentLevel) @trusted { 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 JSONType.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(' '); toValueImpl(member, indentLevel + 1); } } import std.algorithm.sorting : sort; // https://issues.dlang.org/show_bug.cgi?id=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 JSONType.array: auto arr = value.arrayNoRef; if (arr.empty) { json.put("[]"); } else { putCharAndEOL('['); foreach (i, el; arr) { if (i) putCharAndEOL(','); putTabs(1); toValueImpl(el, indentLevel + 1); } putEOL(); putTabs(); json.put(']'); } break; case JSONType.string: toString(value.str); break; case JSONType.integer: json.put(to!string(value.store.integer)); break; case JSONType.uinteger: json.put(to!string(value.store.uinteger)); break; case JSONType.float_: import std.math.traits : 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.algorithm.searching : canFind; import std.format : sformat; // 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) char[25] buf; auto result = buf[].sformat!"%.18g"(val); json.put(result); if (!result.canFind('e') && !result.canFind('.')) json.put(".0"); } break; case JSONType.true_: json.put("true"); break; case JSONType.false_: json.put("false"); break; case JSONType.null_: json.put("null"); break; } } toValueImpl(root, 0); } // https://issues.dlang.org/show_bug.cgi?id=12897 @safe unittest { 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é"`); } // https://issues.dlang.org/show_bug.cgi?id=20511 @system unittest { import std.format.write : formattedWrite; import std.range : nullSink, outputRangeObject; outputRangeObject!(const(char)[])(nullSink) .formattedWrite!"%s"(JSONValue.init); } // Issue 16432 - JSON incorrectly parses to string @safe unittest { // Floating points numbers are rounded to the nearest integer and thus get // incorrectly parsed import std.math.operations : isClose; string s = "{\"rating\": 3.0 }"; JSONValue j = parseJSON(s); assert(j["rating"].type == JSONType.float_); j = j.toString.parseJSON; assert(j["rating"].type == JSONType.float_); assert(isClose(j["rating"].floating, 3.0)); s = "{\"rating\": -3.0 }"; j = parseJSON(s); assert(j["rating"].type == JSONType.float_); j = j.toString.parseJSON; assert(j["rating"].type == JSONType.float_); assert(isClose(j["rating"].floating, -3.0)); // https://issues.dlang.org/show_bug.cgi?id=13660 auto jv1 = JSONValue(4.0); auto textual = jv1.toString(); auto jv2 = parseJSON(textual); assert(jv1.type == JSONType.float_); assert(textual == "4.0"); assert(jv2.type == JSONType.float_); } @safe unittest { // Adapted from https://github.com/dlang/phobos/pull/5005 // Result from toString is not checked here, because this // might differ (%e-like or %f-like output) depending // on OS and compiler optimization. import std.math.operations : isClose; // test positive extreme values JSONValue j; j["rating"] = 1e18 - 65; assert(isClose(j.toString.parseJSON["rating"].floating, 1e18 - 65)); j["rating"] = 1e18 - 64; assert(isClose(j.toString.parseJSON["rating"].floating, 1e18 - 64)); // negative extreme values j["rating"] = -1e18 + 65; assert(isClose(j.toString.parseJSON["rating"].floating, -1e18 + 65)); j["rating"] = -1e18 + 64; assert(isClose(j.toString.parseJSON["rating"].floating, -1e18 + 64)); } /** 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 == JSONType.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 == JSONType.integer); assertNotThrown(jv.integer); jv = cast(uint) 3; assert(jv.type == JSONType.uinteger); assertNotThrown(jv.uinteger); jv = 3.0; assert(jv.type == JSONType.float_); assertNotThrown(jv.floating); jv = ["key" : "value"]; assert(jv.type == JSONType.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 == JSONType.string); assertNotThrown(value.str); assert(value.str == "value"); } jv = [3, 4, 5]; assert(jv.type == JSONType.array); assertNotThrown(jv.array); assertNotThrown(jv[2]); foreach (size_t index, value; jv) { static assert(is(typeof(value) == JSONValue)); assert(value.type == JSONType.integer); assertNotThrown(value.integer); assert(index == (value.integer-3)); } jv = null; assert(jv.type == JSONType.null_); assert(jv.isNull); jv = "foo"; assert(!jv.isNull); jv = JSONValue("value"); assert(jv.type == JSONType.string); assert(jv.str == "value"); JSONValue jv2 = JSONValue("value"); assert(jv2.type == JSONType.string); assert(jv2.str == "value"); JSONValue jv3 = JSONValue("\u001c"); assert(jv3.type == JSONType.string); assert(jv3.str == "\u001C"); } // https://issues.dlang.org/show_bug.cgi?id=11504 @system unittest { JSONValue jv = 1; assert(jv.type == JSONType.integer); jv.str = "123"; assert(jv.type == JSONType.string); assert(jv.str == "123"); jv.integer = 1; assert(jv.type == JSONType.integer); assert(jv.integer == 1); jv.uinteger = 2u; assert(jv.type == JSONType.uinteger); assert(jv.uinteger == 2u); jv.floating = 1.5; assert(jv.type == JSONType.float_); assert(jv.floating == 1.5); jv.object = ["key" : JSONValue("value")]; assert(jv.type == JSONType.object); assert(jv.object == ["key" : JSONValue("value")]); jv.array = [JSONValue(1), JSONValue(2), JSONValue(3)]; assert(jv.type == JSONType.array); assert(jv.array == [JSONValue(1), JSONValue(2), JSONValue(3)]); jv = true; assert(jv.type == JSONType.true_); jv = false; assert(jv.type == JSONType.false_); enum E{True = true} jv = E.True; assert(jv.type == JSONType.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 { // https://issues.dlang.org/show_bug.cgi?id=12969 JSONValue jv; jv["int"] = 123; assert(jv.type == JSONType.object); assert("int" in jv); assert(jv["int"].integer == 123); jv["array"] = [1, 2, 3, 4, 5]; assert(jv["array"].type == JSONType.array); assert(jv["array"][2].integer == 3); jv["str"] = "D language"; assert(jv["str"].type == JSONType.string); assert(jv["str"].str == "D language"); jv["bool"] = false; assert(jv["bool"].type == JSONType.false_); assert(jv.object.length == 4); jv = [5, 4, 3, 2, 1]; assert(jv.type == JSONType.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.traits : 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); } // https://issues.dlang.org/show_bug.cgi?id=15884 pure nothrow @safe unittest { import std.typecons; void Test(C)() { C[] a = ['x']; JSONValue testVal = a; assert(testVal.type == JSONType.string); testVal = a.idup; assert(testVal.type == JSONType.string); } Test!char(); Test!wchar(); Test!dchar(); } // https://issues.dlang.org/show_bug.cgi?id=15885 @safe unittest { enum bool realInDoublePrecision = real.mant_dig == double.mant_dig; static bool test(const double num0) { import std.math.operations : 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)); } // https://issues.dlang.org/show_bug.cgi?id=17555 @safe unittest { import std.exception : assertThrown; assertThrown!JSONException(parseJSON("\"a\nb\"")); } // https://issues.dlang.org/show_bug.cgi?id=17556 @safe unittest { auto v = JSONValue("\U0001D11E"); auto j = toJSON(v, false, JSONOptions.escapeNonAsciiChars); assert(j == `"\uD834\uDD1E"`); } // https://issues.dlang.org/show_bug.cgi?id=5904 @safe unittest { string s = `"\uD834\uDD1E"`; auto j = parseJSON(s); assert(j.str == "\U0001D11E"); } // https://issues.dlang.org/show_bug.cgi?id=17557 @safe unittest { assert(parseJSON("\"\xFF\"").str == "\xFF"); assert(parseJSON("\"\U0001D11E\"").str == "\U0001D11E"); } // https://issues.dlang.org/show_bug.cgi?id=17553 @safe unittest { 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"); } // JSONOptions.doNotEscapeSlashes (https://issues.dlang.org/show_bug.cgi?id=17587) @safe unittest { assert(parseJSON(`"/"`).toString == `"\/"`); assert(parseJSON(`"\/"`).toString == `"\/"`); assert(parseJSON(`"/"`).toString(JSONOptions.doNotEscapeSlashes) == `"/"`); assert(parseJSON(`"\/"`).toString(JSONOptions.doNotEscapeSlashes) == `"/"`); } // JSONOptions.strictParsing (https://issues.dlang.org/show_bug.cgi?id=16639) @safe unittest { import std.exception : assertThrown; // Unescaped ASCII NULs assert(parseJSON("[\0]").type == JSONType.array); assertThrown!JSONException(parseJSON("[\0]", JSONOptions.strictParsing)); assert(parseJSON("\"\0\"").str == "\0"); assertThrown!JSONException(parseJSON("\"\0\"", JSONOptions.strictParsing)); // Unescaped ASCII DEL (0x7f) in strings assert(parseJSON("\"\x7f\"").str == "\x7f"); assert(parseJSON("\"\x7f\"", JSONOptions.strictParsing).str == "\x7f"); // "true", "false", "null" case sensitivity assert(parseJSON("true").type == JSONType.true_); assert(parseJSON("true", JSONOptions.strictParsing).type == JSONType.true_); assert(parseJSON("True").type == JSONType.true_); assertThrown!JSONException(parseJSON("True", JSONOptions.strictParsing)); assert(parseJSON("tRUE").type == JSONType.true_); assertThrown!JSONException(parseJSON("tRUE", JSONOptions.strictParsing)); assert(parseJSON("false").type == JSONType.false_); assert(parseJSON("false", JSONOptions.strictParsing).type == JSONType.false_); assert(parseJSON("False").type == JSONType.false_); assertThrown!JSONException(parseJSON("False", JSONOptions.strictParsing)); assert(parseJSON("fALSE").type == JSONType.false_); assertThrown!JSONException(parseJSON("fALSE", JSONOptions.strictParsing)); assert(parseJSON("null").type == JSONType.null_); assert(parseJSON("null", JSONOptions.strictParsing).type == JSONType.null_); assert(parseJSON("Null").type == JSONType.null_); assertThrown!JSONException(parseJSON("Null", JSONOptions.strictParsing)); assert(parseJSON("nULL").type == JSONType.null_); assertThrown!JSONException(parseJSON("nULL", JSONOptions.strictParsing)); // Whitespace characters assert(parseJSON("[\f\v]").type == JSONType.array); assertThrown!JSONException(parseJSON("[\f\v]", JSONOptions.strictParsing)); assert(parseJSON("[ \t\r\n]").type == JSONType.array); assert(parseJSON("[ \t\r\n]", JSONOptions.strictParsing).type == JSONType.array); // Empty input assert(parseJSON("").type == JSONType.null_); assertThrown!JSONException(parseJSON("", JSONOptions.strictParsing)); // Numbers with leading '0's assert(parseJSON("01").integer == 1); assertThrown!JSONException(parseJSON("01", JSONOptions.strictParsing)); assert(parseJSON("-01").integer == -1); assertThrown!JSONException(parseJSON("-01", JSONOptions.strictParsing)); assert(parseJSON("0.01").floating == 0.01); assert(parseJSON("0.01", JSONOptions.strictParsing).floating == 0.01); assert(parseJSON("0e1").floating == 0); assert(parseJSON("0e1", JSONOptions.strictParsing).floating == 0); // Trailing characters after JSON value assert(parseJSON(`""asdf`).str == ""); assertThrown!JSONException(parseJSON(`""asdf`, JSONOptions.strictParsing)); assert(parseJSON("987\0").integer == 987); assertThrown!JSONException(parseJSON("987\0", JSONOptions.strictParsing)); assert(parseJSON("987\0\0").integer == 987); assertThrown!JSONException(parseJSON("987\0\0", JSONOptions.strictParsing)); assert(parseJSON("[]]").type == JSONType.array); assertThrown!JSONException(parseJSON("[]]", JSONOptions.strictParsing)); assert(parseJSON("123 \t\r\n").integer == 123); // Trailing whitespace is OK assert(parseJSON("123 \t\r\n", JSONOptions.strictParsing).integer == 123); } @system unittest { import std.algorithm.iteration : map; import std.array : array; import std.exception : assertThrown; string s = `{ "a" : [1,2,3,], }`; JSONValue j = parseJSON(s); assert(j["a"].array().map!(i => i.integer()).array == [1,2,3]); assertThrown(parseJSON(s, -1, JSONOptions.strictParsing)); } @system unittest { import std.algorithm.iteration : map; import std.array : array; import std.exception : assertThrown; string s = `{ "a" : { } , }`; JSONValue j = parseJSON(s); assert("a" in j); auto t = j["a"].object(); assert(t.empty); assertThrown(parseJSON(s, -1, JSONOptions.strictParsing)); } // https://issues.dlang.org/show_bug.cgi?id=20330 @safe unittest { import std.array : appender; string s = `{"a":[1,2,3]}`; JSONValue j = parseJSON(s); auto app = appender!string(); j.toString(app); assert(app.data == s, app.data); } // https://issues.dlang.org/show_bug.cgi?id=20330 @safe unittest { import std.array : appender; import std.format.write : formattedWrite; string s = `{ "a": [ 1, 2, 3 ] }`; JSONValue j = parseJSON(s); auto app = appender!string(); j.toPrettyString(app); assert(app.data == s, app.data); }