Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1496884

Browse files
committedOct 21, 2024··
Fix Bugzilla 24823 - std.json: Allow optionally preserving the order of fields in JSON objects
1 parent f7e523b commit 1496884

File tree

1 file changed

+274
-42
lines changed

1 file changed

+274
-42
lines changed
 

‎std/json.d

+274-42
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ enum JSONOptions
8585
escapeNonAsciiChars = 0x2, /// Encode non-ASCII characters with a Unicode escape sequence
8686
doNotEscapeSlashes = 0x4, /// Do not escape slashes ('/')
8787
strictParsing = 0x8, /// Strictly follow RFC-8259 grammar when parsing
88+
preserveObjectOrder = 0x16, /// Preserve order of object keys when parsing
8889
}
8990

9091
/**
@@ -100,6 +101,7 @@ enum JSONType : byte
100101
float_, /// ditto
101102
array, /// ditto
102103
object, /// ditto
104+
orderedObject, /// ditto
103105
true_, /// ditto
104106
false_, /// ditto
105107
// FIXME: Find some way to deprecate the enum members below, which does NOT
@@ -126,13 +128,21 @@ struct JSONValue
126128
{
127129
import std.exception : enforce;
128130

131+
import std.typecons : Tuple;
132+
133+
alias OrderedObjectMember = Tuple!(
134+
string, "key",
135+
JSONValue, "value",
136+
);
137+
129138
union Store
130139
{
131140
string str;
132141
long integer;
133142
ulong uinteger;
134143
double floating;
135144
JSONValue[string] object;
145+
OrderedObjectMember[] orderedObject;
136146
JSONValue[] array;
137147
}
138148
private Store store;
@@ -310,13 +320,58 @@ struct JSONValue
310320
* ---
311321
*
312322
* Throws: `JSONException` for read access if `type` is not
313-
* `JSONType.object`.
323+
* `JSONType.object` or `JSONType.orderedObject`.
314324
*/
315325
@property inout(JSONValue[string]) objectNoRef() inout pure @trusted
316326
{
317-
enforce!JSONException(type == JSONType.object,
318-
"JSONValue is not an object");
319-
return store.object;
327+
switch (type)
328+
{
329+
case JSONType.object:
330+
return store.object;
331+
case JSONType.orderedObject:
332+
JSONValue[string] result;
333+
foreach (pair; store.orderedObject)
334+
result[pair.key] = pair.value;
335+
return cast(inout)result;
336+
default:
337+
throw new JSONException("JSONValue is not an object or ordered object");
338+
}
339+
}
340+
341+
/***
342+
* Value getter/setter for `JSONType.orderedObject`.
343+
* Throws: `JSONException` for read access if `type` is not
344+
* `JSONType.orderedObject`.
345+
* Note: This is @system because of the following pattern:
346+
---
347+
auto a = &(json.orderedObject());
348+
json.uinteger = 0; // overwrite AA pointer
349+
(*a)["hello"] = "world"; // segmentation fault
350+
---
351+
*/
352+
@property ref inout(OrderedObjectMember[]) orderedObject() inout pure @system return
353+
{
354+
enforce!JSONException(type == JSONType.orderedObject,
355+
"JSONValue is not an orderedObject");
356+
return store.orderedObject;
357+
}
358+
/// ditto
359+
@property OrderedObjectMember[] orderedObject(return scope OrderedObjectMember[] v) pure nothrow @nogc @trusted // TODO make @safe
360+
{
361+
assign(v);
362+
return v;
363+
}
364+
365+
/***
366+
* Value getter for `JSONType.orderedObject`.
367+
* Unlike `orderedObject`, this retrieves the object by value
368+
* and can be used in @safe code.
369+
*/
370+
@property inout(OrderedObjectMember[]) orderedObjectNoRef() inout pure @trusted
371+
{
372+
enforce!JSONException(type == JSONType.orderedObject,
373+
"JSONValue is not an orderedObject");
374+
return store.orderedObject;
320375
}
321376

322377
/***
@@ -527,6 +582,11 @@ struct JSONValue
527582
() @trusted { store.object = aa; }();
528583
}
529584
}
585+
else static if (is(T : OrderedObjectMember[]))
586+
{
587+
type_tag = JSONType.orderedObject;
588+
() @trusted { store.orderedObject = arg; }();
589+
}
530590
else static if (isArray!T)
531591
{
532592
type_tag = JSONType.array;
@@ -629,6 +689,32 @@ struct JSONValue
629689
assert(obj1 != obj2);
630690
}
631691

692+
/**
693+
* An enum value that can be used to obtain a `JSONValue` representing
694+
* an empty JSON object.
695+
* Unlike `emptyObject`, the order of inserted keys is preserved.
696+
*/
697+
enum emptyOrderedObject = {
698+
JSONValue v;
699+
v.orderedObject = null;
700+
return v;
701+
}();
702+
///
703+
@system unittest
704+
{
705+
JSONValue obj = JSONValue.emptyOrderedObject;
706+
assert(obj.type == JSONType.orderedObject);
707+
obj["b"] = JSONValue(2);
708+
obj["a"] = JSONValue(1);
709+
assert(obj["a"] == JSONValue(1));
710+
assert(obj["b"] == JSONValue(2));
711+
712+
string[] keys;
713+
foreach (string k, JSONValue v; obj)
714+
keys ~= k;
715+
assert(keys == ["b", "a"]);
716+
}
717+
632718
/**
633719
* An enum value that can be used to obtain a `JSONValue` representing
634720
* an empty JSON array.
@@ -703,21 +789,39 @@ struct JSONValue
703789
* initializes it with a JSON object and then performs
704790
* the index assignment.
705791
*
706-
* Throws: `JSONException` if `type` is not `JSONType.object`
707-
* or `JSONType.null_`.
792+
* Throws: `JSONException` if `type` is not `JSONType.object`,
793+
* `JSONType.orderedObject`, or `JSONType.null_`.
708794
*/
709795
void opIndexAssign(T)(auto ref T value, string key)
710796
{
711-
enforce!JSONException(type == JSONType.object || type == JSONType.null_,
712-
"JSONValue must be object or null");
713-
JSONValue[string] aa = null;
714-
if (type == JSONType.object)
797+
enforce!JSONException(
798+
type == JSONType.object ||
799+
type == JSONType.orderedObject ||
800+
type == JSONType.null_,
801+
"JSONValue must be object or null");
802+
if (type == JSONType.orderedObject)
715803
{
716-
aa = this.objectNoRef;
804+
auto arr = this.orderedObjectNoRef;
805+
foreach (ref pair; arr)
806+
if (pair.key == key)
807+
{
808+
pair.value = value;
809+
return;
810+
}
811+
arr ~= OrderedObjectMember(key, JSONValue(value));
812+
this.orderedObject = arr;
717813
}
814+
else
815+
{
816+
JSONValue[string] aa = null;
817+
if (type == JSONType.object)
818+
{
819+
aa = this.objectNoRef;
820+
}
718821

719-
aa[key] = value;
720-
this.object = aa;
822+
aa[key] = value;
823+
this.object = aa;
824+
}
721825
}
722826
///
723827
@safe unittest
@@ -828,6 +932,8 @@ struct JSONValue
828932
/// ditto
829933
bool opEquals(ref const JSONValue rhs) const @nogc nothrow pure @trusted
830934
{
935+
import std.algorithm.searching : canFind;
936+
831937
// Default doesn't work well since store is a union. Compare only
832938
// what should be in store.
833939
// This is @trusted to remain nogc, nothrow, fast, and usable from @safe code.
@@ -873,7 +979,40 @@ struct JSONValue
873979
case JSONType.string:
874980
return type_tag == rhs.type_tag && store.str == rhs.store.str;
875981
case JSONType.object:
876-
return type_tag == rhs.type_tag && store.object == rhs.store.object;
982+
switch (rhs.type_tag)
983+
{
984+
case JSONType.object:
985+
return store.object == rhs.store.object;
986+
case JSONType.orderedObject:
987+
if (store.object.length != rhs.store.orderedObject.length)
988+
return false;
989+
foreach (ref pair; rhs.store.orderedObject)
990+
if (pair.key !in store.object || store.object[pair.key] != pair.value)
991+
return false;
992+
return true;
993+
default:
994+
return false;
995+
}
996+
case JSONType.orderedObject:
997+
switch (rhs.type_tag)
998+
{
999+
case JSONType.object:
1000+
if (store.orderedObject.length != rhs.store.object.length)
1001+
return false;
1002+
foreach (ref pair; store.orderedObject)
1003+
if (pair.key !in rhs.store.object || rhs.store.object[pair.key] != pair.value)
1004+
return false;
1005+
return true;
1006+
case JSONType.orderedObject:
1007+
if (store.orderedObject.length != rhs.store.orderedObject.length)
1008+
return false;
1009+
foreach (ref pair; store.orderedObject)
1010+
if (!rhs.store.orderedObject.canFind(pair))
1011+
return false;
1012+
return true;
1013+
default:
1014+
return false;
1015+
}
8771016
case JSONType.array:
8781017
return type_tag == rhs.type_tag && store.array == rhs.store.array;
8791018
case JSONType.true_:
@@ -913,18 +1052,35 @@ struct JSONValue
9131052
/// Implements the foreach `opApply` interface for json objects.
9141053
int opApply(scope int delegate(string key, ref JSONValue) dg) @system
9151054
{
916-
enforce!JSONException(type == JSONType.object,
917-
"JSONValue is not an object");
918-
int result;
919-
920-
foreach (string key, ref value; object)
1055+
switch (type)
9211056
{
922-
result = dg(key, value);
923-
if (result)
924-
break;
925-
}
1057+
case JSONType.object:
1058+
int result;
9261059

927-
return result;
1060+
foreach (string key, ref value; object)
1061+
{
1062+
result = dg(key, value);
1063+
if (result)
1064+
break;
1065+
}
1066+
1067+
return result;
1068+
1069+
case JSONType.orderedObject:
1070+
int result;
1071+
1072+
foreach (ref pair; orderedObject)
1073+
{
1074+
result = dg(pair.key, pair.value);
1075+
if (result)
1076+
break;
1077+
}
1078+
1079+
return result;
1080+
1081+
default:
1082+
throw new JSONException("JSONValue is not an object or orderedObject");
1083+
}
9281084
}
9291085

9301086
/***
@@ -1018,6 +1174,7 @@ if (isSomeFiniteCharInputRange!T)
10181174
Nullable!Char next;
10191175
int line = 1, pos = 0;
10201176
immutable bool strict = (options & JSONOptions.strictParsing) != 0;
1177+
immutable bool ordered = (options & JSONOptions.preserveObjectOrder) != 0;
10211178

10221179
void error(string msg)
10231180
{
@@ -1258,31 +1415,62 @@ if (isSomeFiniteCharInputRange!T)
12581415
switch (c)
12591416
{
12601417
case '{':
1261-
if (testChar('}'))
1418+
if (ordered)
12621419
{
1263-
value.object = null;
1264-
break;
1265-
}
1420+
if (testChar('}'))
1421+
{
1422+
value.orderedObject = null;
1423+
break;
1424+
}
12661425

1267-
JSONValue[string] obj;
1268-
do
1426+
JSONValue.OrderedObjectMember[] obj;
1427+
do
1428+
{
1429+
skipWhitespace();
1430+
if (!strict && peekChar() == '}')
1431+
{
1432+
break;
1433+
}
1434+
checkChar('"');
1435+
string name = parseString();
1436+
checkChar(':');
1437+
JSONValue member;
1438+
parseValue(member);
1439+
obj ~= JSONValue.OrderedObjectMember(name, member);
1440+
}
1441+
while (testChar(','));
1442+
value.orderedObject = obj;
1443+
1444+
checkChar('}');
1445+
}
1446+
else
12691447
{
1270-
skipWhitespace();
1271-
if (!strict && peekChar() == '}')
1448+
if (testChar('}'))
12721449
{
1450+
value.object = null;
12731451
break;
12741452
}
1275-
checkChar('"');
1276-
string name = parseString();
1277-
checkChar(':');
1278-
JSONValue member;
1279-
parseValue(member);
1280-
obj[name] = member;
1281-
}
1282-
while (testChar(','));
1283-
value.object = obj;
12841453

1285-
checkChar('}');
1454+
JSONValue[string] obj;
1455+
do
1456+
{
1457+
skipWhitespace();
1458+
if (!strict && peekChar() == '}')
1459+
{
1460+
break;
1461+
}
1462+
checkChar('"');
1463+
string name = parseString();
1464+
checkChar(':');
1465+
JSONValue member;
1466+
parseValue(member);
1467+
obj[name] = member;
1468+
}
1469+
while (testChar(','));
1470+
value.object = obj;
1471+
1472+
checkChar('}');
1473+
}
12861474
break;
12871475

12881476
case '[':
@@ -1684,6 +1872,36 @@ if (isOutputRange!(Out,char))
16841872
}
16851873
break;
16861874

1875+
case JSONType.orderedObject:
1876+
auto obj = value.orderedObjectNoRef;
1877+
if (!obj.length)
1878+
{
1879+
json.put("{}");
1880+
}
1881+
else
1882+
{
1883+
putCharAndEOL('{');
1884+
bool first = true;
1885+
1886+
foreach (pair; obj)
1887+
{
1888+
if (!first)
1889+
putCharAndEOL(',');
1890+
first = false;
1891+
putTabs(1);
1892+
toString(pair.key);
1893+
json.put(':');
1894+
if (pretty)
1895+
json.put(' ');
1896+
toValueImpl(pair.value, indentLevel + 1);
1897+
}
1898+
1899+
putEOL();
1900+
putTabs();
1901+
json.put('}');
1902+
}
1903+
break;
1904+
16871905
case JSONType.array:
16881906
auto arr = value.arrayNoRef;
16891907
if (arr.empty)
@@ -2469,3 +2687,17 @@ pure nothrow @safe unittest
24692687

24702688
assert(app.data == s, app.data);
24712689
}
2690+
2691+
// https://issues.dlang.org/show_bug.cgi?id=24823 - JSONOptions.preserveObjectOrder
2692+
@safe unittest
2693+
{
2694+
import std.array : appender;
2695+
2696+
string s = `{"b":2,"a":1}`;
2697+
JSONValue j = parseJSON(s, -1, JSONOptions.preserveObjectOrder);
2698+
2699+
auto app = appender!string();
2700+
j.toString(app);
2701+
2702+
assert(app.data == s, app.data);
2703+
}

0 commit comments

Comments
 (0)
Please sign in to comment.