Skip to content

Commit 9991f6a

Browse files
authored
Merge pull request #2713 from spevans/pr_json_writing_options
JSONSerialization: Add support for .withoutEscapingSlashes, .fragmentsAllowed
2 parents 7c8f145 + 09fca84 commit 9991f6a

File tree

4 files changed

+83
-20
lines changed

4 files changed

+83
-20
lines changed

Sources/Foundation/JSONEncoder.swift

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -238,15 +238,8 @@ open class JSONEncoder {
238238
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values."))
239239
}
240240

241-
if topLevel is NSNull {
242-
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as null JSON fragment."))
243-
} else if topLevel is NSNumber {
244-
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as number JSON fragment."))
245-
} else if topLevel is NSString {
246-
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as string JSON fragment."))
247-
}
241+
let writingOptions = JSONSerialization.WritingOptions(rawValue: self.outputFormatting.rawValue).union(.fragmentsAllowed)
248242

249-
let writingOptions = JSONSerialization.WritingOptions(rawValue: self.outputFormatting.rawValue)
250243
do {
251244
return try JSONSerialization.data(withJSONObject: topLevel, options: writingOptions)
252245
} catch {
@@ -1155,7 +1148,7 @@ open class JSONDecoder {
11551148
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
11561149
let topLevel: Any
11571150
do {
1158-
topLevel = try JSONSerialization.jsonObject(with: data)
1151+
topLevel = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
11591152
} catch {
11601153
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
11611154
}

Sources/Foundation/JSONSerialization.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ extension JSONSerialization {
2525

2626
public static let prettyPrinted = WritingOptions(rawValue: 1 << 0)
2727
public static let sortedKeys = WritingOptions(rawValue: 1 << 1)
28+
public static let fragmentsAllowed = WritingOptions(rawValue: 1 << 2)
29+
public static let withoutEscapingSlashes = WritingOptions(rawValue: 1 << 3)
2830
}
2931
}
3032

@@ -139,8 +141,7 @@ open class JSONSerialization : NSObject {
139141
var jsonStr = [UInt8]()
140142

141143
var writer = JSONWriter(
142-
pretty: opt.contains(.prettyPrinted),
143-
sortedKeys: opt.contains(.sortedKeys),
144+
options: opt,
144145
writer: { (str: String?) in
145146
if let str = str {
146147
jsonStr.append(contentsOf: str.utf8)
@@ -157,7 +158,10 @@ open class JSONSerialization : NSObject {
157158
} else if let container = value as? Dictionary<AnyHashable, Any> {
158159
try writer.serializeJSON(container)
159160
} else {
160-
fatalError("Top-level object was not NSArray or NSDictionary") // This is a fatal error in objective-c too (it is an NSInvalidArgumentException)
161+
guard opt.contains(.fragmentsAllowed) else {
162+
fatalError("Top-level object was not NSArray or NSDictionary") // This is a fatal error in objective-c too (it is an NSInvalidArgumentException)
163+
}
164+
try writer.serializeJSON(value)
161165
}
162166

163167
let count = jsonStr.count
@@ -302,11 +306,13 @@ private struct JSONWriter {
302306
var indent = 0
303307
let pretty: Bool
304308
let sortedKeys: Bool
309+
let withoutEscapingSlashes: Bool
305310
let writer: (String?) -> Void
306311

307-
init(pretty: Bool = false, sortedKeys: Bool = false, writer: @escaping (String?) -> Void) {
308-
self.pretty = pretty
309-
self.sortedKeys = sortedKeys
312+
init(options: JSONSerialization.WritingOptions, writer: @escaping (String?) -> Void) {
313+
pretty = options.contains(.prettyPrinted)
314+
sortedKeys = options.contains(.sortedKeys)
315+
withoutEscapingSlashes = options.contains(.withoutEscapingSlashes)
310316
self.writer = writer
311317
}
312318

@@ -380,7 +386,8 @@ private struct JSONWriter {
380386
case "\\":
381387
writer("\\\\") // U+005C reverse solidus
382388
case "/":
383-
writer("\\/") // U+002F solidus
389+
if !withoutEscapingSlashes { writer("\\") }
390+
writer("/") // U+002F solidus
384391
case "\u{8}":
385392
writer("\\b") // U+0008 backspace
386393
case "\u{c}":

Tests/Foundation/Tests/TestJSONEncoder.swift

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,40 @@ struct TopLevelObjectWrapper<T: Codable & Equatable>: Codable, Equatable {
2121

2222
class TestJSONEncoder : XCTestCase {
2323

24+
// MARK: - Encoding Top-Level fragments
25+
func test_encodingTopLevelFragments() {
26+
27+
func _testFragment<T: Codable & Equatable>(value: T, fragment: String) {
28+
let data: Data
29+
let payload: String
30+
31+
do {
32+
data = try JSONEncoder().encode(value)
33+
payload = try XCTUnwrap(String.init(decoding: data, as: UTF8.self))
34+
XCTAssertEqual(fragment, payload)
35+
} catch {
36+
XCTFail("Failed to encode \(T.self) to JSON: \(error)")
37+
return
38+
}
39+
do {
40+
let decodedValue = try JSONDecoder().decode(T.self, from: data)
41+
XCTAssertEqual(value, decodedValue)
42+
} catch {
43+
XCTFail("Failed to decode \(payload) to \(T.self): \(error)")
44+
}
45+
}
46+
47+
_testFragment(value: 2, fragment: "2")
48+
_testFragment(value: false, fragment: "false")
49+
_testFragment(value: true, fragment: "true")
50+
_testFragment(value: Float(1), fragment: "1")
51+
_testFragment(value: Double(2), fragment: "2")
52+
_testFragment(value: Decimal(Double.leastNormalMagnitude), fragment: "0.0000000000000000000000000000000000000000000000000002225073858507201792")
53+
_testFragment(value: "test", fragment: "\"test\"")
54+
let v: Int? = nil
55+
_testFragment(value: v, fragment: "null")
56+
}
57+
2458
// MARK: - Encoding Top-Level Empty Types
2559
func test_encodingTopLevelEmptyStruct() {
2660
let empty = EmptyStruct()
@@ -34,20 +68,20 @@ class TestJSONEncoder : XCTestCase {
3468

3569
// MARK: - Encoding Top-Level Single-Value Types
3670
func test_encodingTopLevelSingleValueEnum() {
37-
_testEncodeFailure(of: Switch.off)
38-
_testEncodeFailure(of: Switch.on)
71+
_testRoundTrip(of: Switch.off)
72+
_testRoundTrip(of: Switch.on)
3973

4074
_testRoundTrip(of: TopLevelArrayWrapper(Switch.off))
4175
_testRoundTrip(of: TopLevelArrayWrapper(Switch.on))
4276
}
4377

4478
func test_encodingTopLevelSingleValueStruct() {
45-
_testEncodeFailure(of: Timestamp(3141592653))
79+
_testRoundTrip(of: Timestamp(3141592653))
4680
_testRoundTrip(of: TopLevelArrayWrapper(Timestamp(3141592653)))
4781
}
4882

4983
func test_encodingTopLevelSingleValueClass() {
50-
_testEncodeFailure(of: Counter())
84+
_testRoundTrip(of: Counter())
5185
_testRoundTrip(of: TopLevelArrayWrapper(Counter()))
5286
}
5387

@@ -492,6 +526,11 @@ class TestJSONEncoder : XCTestCase {
492526
}
493527
}
494528

529+
func test_codingOfNil() {
530+
let x: Int? = nil
531+
test_codingOf(value: x, toAndFrom: "null")
532+
}
533+
495534
func test_codingOfInt8() {
496535
test_codingOf(value: Int8(-42), toAndFrom: "-42")
497536
}
@@ -1364,6 +1403,7 @@ fileprivate struct JSON: Equatable {
13641403
extension TestJSONEncoder {
13651404
static var allTests: [(String, (TestJSONEncoder) -> () throws -> Void)] {
13661405
return [
1406+
("test_encodingTopLevelFragments", test_encodingTopLevelFragments),
13671407
("test_encodingTopLevelEmptyStruct", test_encodingTopLevelEmptyStruct),
13681408
("test_encodingTopLevelEmptyClass", test_encodingTopLevelEmptyClass),
13691409
("test_encodingTopLevelSingleValueEnum", test_encodingTopLevelSingleValueEnum),
@@ -1393,6 +1433,7 @@ extension TestJSONEncoder {
13931433
("test_nestedContainerCodingPaths", test_nestedContainerCodingPaths),
13941434
("test_superEncoderCodingPaths", test_superEncoderCodingPaths),
13951435
("test_codingOfBool", test_codingOfBool),
1436+
("test_codingOfNil", test_codingOfNil),
13961437
("test_codingOfInt8", test_codingOfInt8),
13971438
("test_codingOfUInt8", test_codingOfUInt8),
13981439
("test_codingOfInt16", test_codingOfInt16),

Tests/Foundation/Tests/TestJSONSerialization.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,8 @@ extension TestJSONSerialization {
10131013
("test_serialize_Decimal", test_serialize_Decimal),
10141014
("test_serialize_NSDecimalNumber", test_serialize_NSDecimalNumber),
10151015
("test_serialize_stringEscaping", test_serialize_stringEscaping),
1016+
("test_serialize_fragments", test_serialize_fragments),
1017+
("test_serialize_withoutEscapingSlashes", test_serialize_withoutEscapingSlashes),
10161018
("test_jsonReadingOffTheEndOfBuffers", test_jsonReadingOffTheEndOfBuffers),
10171019
("test_jsonObjectToOutputStreamBuffer", test_jsonObjectToOutputStreamBuffer),
10181020
("test_jsonObjectToOutputStreamFile", test_jsonObjectToOutputStreamFile),
@@ -1371,6 +1373,26 @@ extension TestJSONSerialization {
13711373
XCTAssertEqual(try trySerialize(json), "[\"j\\/\"]")
13721374
}
13731375

1376+
func test_serialize_fragments() {
1377+
XCTAssertEqual(try trySerialize(2, options: .fragmentsAllowed), "2")
1378+
XCTAssertEqual(try trySerialize(false, options: .fragmentsAllowed), "false")
1379+
XCTAssertEqual(try trySerialize(true, options: .fragmentsAllowed), "true")
1380+
XCTAssertEqual(try trySerialize(Float(1), options: .fragmentsAllowed), "1")
1381+
XCTAssertEqual(try trySerialize(Double(2), options: .fragmentsAllowed), "2")
1382+
XCTAssertEqual(try trySerialize(Decimal(Double.leastNormalMagnitude), options: .fragmentsAllowed), "0.0000000000000000000000000000000000000000000000000002225073858507201792")
1383+
XCTAssertEqual(try trySerialize("test", options: .fragmentsAllowed), "\"test\"")
1384+
}
1385+
1386+
func test_serialize_withoutEscapingSlashes() {
1387+
// .withoutEscapingSlashes controls whether a "/" is encoded as "\\/" or "/"
1388+
let testString = "This /\\/ is a \\ \\\\ \\\\\\ \"string\"\n\r\t\u{0}\u{1}\u{8}\u{c}\u{f}"
1389+
let escapedString = "\"This \\/\\\\\\/ is a \\\\ \\\\\\\\ \\\\\\\\\\\\ \\\"string\\\"\\n\\r\\t\\u0000\\u0001\\b\\f\\u000f\""
1390+
let unescapedString = "\"This /\\\\/ is a \\\\ \\\\\\\\ \\\\\\\\\\\\ \\\"string\\\"\\n\\r\\t\\u0000\\u0001\\b\\f\\u000f\""
1391+
1392+
XCTAssertEqual(try trySerialize(testString, options: .fragmentsAllowed), escapedString)
1393+
XCTAssertEqual(try trySerialize(testString, options: [.withoutEscapingSlashes, .fragmentsAllowed]), unescapedString)
1394+
}
1395+
13741396
/* These are a programming error and should not be done
13751397
Ideally the interface for JSONSerialization should at compile time prevent this type of thing
13761398
by overloading the interface such that it can only accept dictionaries and arrays.

0 commit comments

Comments
 (0)