blob: 8ba0f05f7826661dfe8b795305aff94aa840dba3 [file] [log] [blame]
// 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) == "\"\&lt;\&gt;\"");
assert(val.to!string() == "\"\&lt;\&gt;\"");
val = parseJSON(`"\u0391\u0392\u0393"`);
assert(toJSON(val) == "\"\&Alpha;\&Beta;\&Gamma;\"");
assert(val.to!string() == "\"\&Alpha;\&Beta;\&Gamma;\"");
val = parseJSON(`"\u2660\u2666"`);
assert(toJSON(val) == "\"\&spades;\&diams;\"");
assert(val.to!string() == "\"\&spades;\&diams;\"");
//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) == `"/"`);
}