Skip to content

Commit e49aa6c

Browse files
Added JSON serialization mechanism
1 parent 2212846 commit e49aa6c

File tree

2 files changed

+295
-2
lines changed

2 files changed

+295
-2
lines changed

Foundation/NSJSONSerialization.swift

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ public class NSJSONSerialization : NSObject {
9696

9797
/* Generate JSON data from a Foundation object. If the object will not produce valid JSON then an exception will be thrown. Setting the NSJSONWritingPrettyPrinted option will generate JSON with whitespace designed to make the output more readable. If that option is not set, the most compact possible JSON will be generated. If an error occurs, the error parameter will be set and the return value will be nil. The resulting data is a encoded in UTF-8.
9898
*/
99-
public class func dataWithJSONObject(obj: AnyObject, options opt: NSJSONWritingOptions) throws -> NSData {
100-
NSUnimplemented()
99+
public class func dataWithJSONObject(obj: Any, options opt: NSJSONWritingOptions) throws -> NSData {
100+
let (newIndent, newLine, spacing) = opt.contains(NSJSONWritingOptions.PrettyPrinted) ? ("\t", "\n", " ") : ("", "", "")
101+
let str = NSString(try JSONSerialize(obj, newIndent: newIndent, newLine: newLine, space: spacing, topLevel: true))
102+
return str.dataUsingEncoding(NSUTF8StringEncoding)!
101103
}
102104

103105
/* Create a Foundation object from JSON data. Set the NSJSONReadingAllowFragments option if the parser should allow top-level objects that are not an NSArray or NSDictionary. Setting the NSJSONReadingMutableContainers option will make the parser generate mutable NSArrays and NSDictionaries. Setting the NSJSONReadingMutableLeaves option will make the parser generate mutable NSString objects. If an error occurs during the parse, then the error parameter will be set and the result will be nil.
@@ -147,6 +149,119 @@ public class NSJSONSerialization : NSObject {
147149
}
148150
}
149151

152+
// MARK: - Serialization
153+
private extension NSJSONSerialization {
154+
155+
private class func escapeStringForJSON(string: String) -> String {
156+
return NSString(NSString(string).stringByReplacingOccurrencesOfString("\\", withString: "\\\\")).stringByReplacingOccurrencesOfString("\"", withString: "\\\"")
157+
}
158+
private class func serializeArray<T>(array: [T], newIndent: String, newLine: String, space: String, indentation: String) throws -> String {
159+
guard array.count > 0
160+
else {
161+
return "\(indentation)[]"
162+
}
163+
var index = 0
164+
return try array.reduce("\(indentation)[\(newLine)", combine: {
165+
index += 1
166+
let separator = (index == array.count) ? "\(newLine)\(indentation)]" : ",\(newLine)"
167+
return $0 + (try JSONSerialize($1, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation + newIndent)) + separator
168+
})
169+
}
170+
171+
private class func serializePOD<T: Any>(object: T, indentation: String) throws -> String {
172+
if let str = object as? String {
173+
return indentation + "\"\(escapeStringForJSON(str))\""
174+
} else if let _ = object as? NSNull {
175+
return indentation + "null"
176+
} else if let obj = object as? CustomStringConvertible {
177+
// Using CustomStringConvertible is a hack to allow for all POD types
178+
// Once bridging works we can revert back.
179+
// TODO: Fix when bridging works
180+
return indentation + escapeStringForJSON(obj.description)
181+
}
182+
else if let num = object as? NSNumber {
183+
return num.description
184+
} else {
185+
throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListWriteInvalidError.rawValue, userInfo: [
186+
"NSDebugDescription" : "Cannot serialize \(object.dynamicType)"
187+
])
188+
}
189+
}
190+
191+
private class func serializeObject<T>(object: [String: T], newIndent: String, newLine: String, space: String, indentation: String) throws -> String {
192+
guard object.count > 0
193+
else {
194+
return "\(indentation){}"
195+
}
196+
var index = 0
197+
return try object.reduce("\(indentation){\(newLine)", combine: {
198+
let valueString : String
199+
do {
200+
valueString = try serializePOD($1.1, indentation: "")
201+
}
202+
catch {
203+
valueString = try JSONSerialize($1.1, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation + newIndent)
204+
}
205+
index += 1
206+
let separator = (index == object.count) ? "\(newLine)\(indentation)}" : ",\(newLine)"
207+
return $0 + "\(indentation + newIndent)\"" + escapeStringForJSON($1.0) + "\"\(space):\(space)" + valueString + separator
208+
})
209+
}
210+
211+
private class func JSONSerialize<T>(object: T, newIndent: String, newLine: String, space: String, topLevel: Bool = false, indentation: String = "") throws -> String {
212+
// TODO: - revisit this once bridging story gets fully figured out
213+
if let array = object as? [Any] {
214+
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
215+
} else if let array = object as? NSArray {
216+
return try serializeArray(array.bridge(), newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
217+
} else if let array = object as? [NSNumber] {
218+
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
219+
} else if let array = object as? [String] {
220+
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
221+
} else if let array = object as? [Double] {
222+
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
223+
} else if let array = object as? [Int] {
224+
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
225+
} else if let array = object as? [Bool] {
226+
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
227+
} else if let array = object as? [Float] {
228+
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
229+
} else if let array = object as? [UInt] {
230+
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
231+
} else if let dict = object as? [String : Any] {
232+
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
233+
} else if let dict = object as? NSDictionary {
234+
return try JSONSerialize(dict.bridge(), newIndent: newIndent, newLine: newLine, space: space, topLevel: topLevel, indentation: indentation)
235+
} else if let dict = object as? [String: String] {
236+
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
237+
} else if let dict = object as? [String: Double] {
238+
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
239+
} else if let dict = object as? [String: Int] {
240+
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
241+
} else if let dict = object as? [String: Bool] {
242+
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
243+
} else if let dict = object as? [String: Float] {
244+
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
245+
} else if let dict = object as? [String: UInt] {
246+
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
247+
} else if let dict = object as? [String: NSNumber] {
248+
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
249+
}
250+
// This else if situation will greatly improve once bridging works properly.
251+
// For now we check for Floats, Doubles, Int, UInt, Bool and NSNumber
252+
// but this may disallow other types like CGFloat, Int32, etc which is a problem
253+
else {
254+
guard !topLevel
255+
else {
256+
throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListWriteInvalidError.rawValue, userInfo: [
257+
"NSDebugDescription" : "Unable to use \(object.dynamicType) as a top level JSON object."
258+
])
259+
}
260+
return try serializePOD(object, indentation: indentation)
261+
}
262+
}
263+
}
264+
150265
//MARK: - Encoding Detection
151266

152267
internal extension NSJSONSerialization {

TestFoundation/TestNSJSONSerialization.swift

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class TestNSJSONSerialization : XCTestCase {
2828
static var allTests: [(String, TestNSJSONSerialization -> () throws -> Void)] {
2929
return JSONObjectWithDataTests
3030
+ deserializationTests
31+
+ serializationTests
3132
+ isValidJSONObjectTests
3233
}
3334

@@ -452,6 +453,183 @@ extension TestNSJSONSerialization {
452453
}
453454

454455
}
456+
// MARK: - JSONSerialization
457+
extension TestNSJSONSerialization {
458+
459+
class var serializationTests: [(String, TestNSJSONSerialization -> () throws -> Void)] {
460+
return [
461+
("test_serialize_emptyObject", test_serialize_emptyObject),
462+
("test_serialize_multiStringObject", test_serialize_multiStringObject),
463+
("test_serialize_complicatedObject", test_serialize_complicatedObject),
464+
465+
("test_serialize_emptyArray", test_serialize_emptyArray),
466+
("test_serialize_multiPODArray", test_serialize_multiPODArray),
467+
468+
("test_serialize_nested", test_serialize_nested),
469+
("test_invalid_json", test_invalidJSON)
470+
]
471+
}
472+
473+
func trySerializeHelper(subject: Any, options: NSJSONWritingOptions) throws -> String {
474+
let data = try NSJSONSerialization.dataWithJSONObject(subject, options: options)
475+
guard let string = NSString(data: data, encoding: NSUTF8StringEncoding)
476+
else {
477+
XCTFail("Unable to convert data to string")
478+
return ""
479+
}
480+
return string.bridge()
481+
}
482+
func trySerialize(subject: Any) throws -> String {
483+
return try trySerializeHelper(subject, options: [])
484+
}
485+
func trySerializePretty(subject: Any) throws -> String {
486+
return try trySerializeHelper(subject, options: [.PrettyPrinted])
487+
}
488+
489+
//MARK: - Object Serialization
490+
func test_serialize_emptyObject() {
491+
492+
XCTAssertEqual(try trySerialize([String: Any]()), "{}")
493+
XCTAssertEqual(try trySerializePretty([String: Any]()), "{}")
494+
495+
XCTAssertEqual(try trySerialize([String: NSNumber]()), "{}")
496+
XCTAssertEqual(try trySerialize([String: String]()), "{}")
497+
498+
var json = [String: Double]()
499+
json["s"] = 1.0
500+
json["s"] = nil
501+
XCTAssertEqual(try trySerialize(json), "{}")
502+
XCTAssertEqual(try trySerializePretty(json), "{}")
503+
}
504+
func test_serialize_multiStringObject() {
505+
506+
var json = [String: String]()
507+
json["hello"] = "world"
508+
XCTAssertEqual(try trySerialize(json), "{\"hello\":\"world\"}")
509+
XCTAssertEqual(try trySerializePretty(json), "{\n\t\"hello\" : \"world\"\n}")
510+
511+
// testing escaped characters like " and \
512+
json["swift"] = "is \\ \"awesome\""
513+
XCTAssertEqual(try trySerialize(json), "{\"hello\":\"world\",\"swift\":\"is \\\\ \\\"awesome\\\"\"}")
514+
XCTAssertEqual(try trySerializePretty(json), "{\n\t\"hello\" : \"world\",\n\t\"swift\" : \"is \\\\ \\\"awesome\\\"\"\n}")
515+
}
516+
func test_serialize_complicatedObject() {
517+
let json : [String: Any] = ["a": 4.0, "b": NSNull(), "c": "string", "d": false]
518+
519+
var normalString = "{"
520+
var prettyString = "{"
521+
for key in json.keys {
522+
normalString += "\"\(key)\":"
523+
prettyString += "\n\t\"\(key)\" : "
524+
let valueString : String
525+
if key == "a" {
526+
valueString = "4.0"
527+
} else if key == "b" {
528+
valueString = "null"
529+
} else if key == "c" {
530+
valueString = "\"string\""
531+
} else {
532+
valueString = "false"
533+
}
534+
normalString += "\(valueString),"
535+
prettyString += "\(valueString),"
536+
}
537+
normalString = normalString.substringToIndex(normalString.endIndex.predecessor()) + "}"
538+
prettyString = prettyString.substringToIndex(prettyString.endIndex.predecessor()) + "\n}"
539+
XCTAssertEqual(try trySerialize(json), normalString)
540+
XCTAssertEqual(try trySerializePretty(json), prettyString)
541+
}
542+
543+
//MARK: - Array Serialization
544+
func test_serialize_emptyArray() {
545+
546+
XCTAssertEqual(try trySerialize([String]()), "[]")
547+
XCTAssertEqual(try trySerializePretty([String]()), "[]")
548+
549+
XCTAssertEqual(try trySerialize([NSNumber]()), "[]")
550+
XCTAssertEqual(try trySerializePretty([NSNumber]()), "[]")
551+
552+
XCTAssertEqual(try trySerialize([Any]()), "[]")
553+
}
554+
func test_serialize_multiPODArray() {
555+
XCTAssertEqual(try trySerialize(["hello", "swift⚡️"]), "[\"hello\",\"swift⚡️\"]")
556+
XCTAssertEqual(try trySerializePretty(["hello", "swift⚡️"]), "[\n\t\"hello\",\n\t\"swift⚡️\"\n]")
557+
558+
XCTAssertEqual(try trySerialize([1.0, 3.3, 2.3]), "[1.0,3.3,2.3]")
559+
XCTAssertEqual(try trySerializePretty([1.0, 3.3, 2.3]), "[\n\t1.0,\n\t3.3,\n\t2.3\n]")
560+
561+
562+
let array: [Any] = [NSNull(), "hello", 1.0]
563+
XCTAssertEqual(try trySerialize(array), "[null,\"hello\",1.0]")
564+
XCTAssertEqual(try trySerializePretty(array), "[\n\tnull,\n\t\"hello\",\n\t1.0\n]")
565+
}
566+
567+
//MARK: - Nested Serialization
568+
func test_serialize_nested() {
569+
let first: [String: Any] = ["a": 4.0, "b": NSNull(), "c": "string", "d": false]
570+
571+
var normalString = "{"
572+
var prettyString = "\n\t{"
573+
574+
for key in first.keys {
575+
normalString += "\"\(key)\":"
576+
prettyString += "\n\t\t\"\(key)\" : "
577+
let valueString : String
578+
if key == "a" {
579+
valueString = "4.0"
580+
} else if key == "b" {
581+
valueString = "null"
582+
} else if key == "c" {
583+
valueString = "\"string\""
584+
} else {
585+
valueString = "false"
586+
}
587+
normalString += "\(valueString),"
588+
prettyString += "\(valueString),"
589+
}
590+
normalString = normalString.substringToIndex(normalString.endIndex.predecessor()) + "}"
591+
prettyString = prettyString.substringToIndex(prettyString.endIndex.predecessor()) + "\n\t}"
592+
593+
let second: [Any] = [NSNull(), "hello", 1.0]
594+
let json: [Any] = [first, second, NSNull(), "hello", 1.0]
595+
596+
XCTAssertEqual(try trySerialize(json), "[\(normalString),[null,\"hello\",1.0],null,\"hello\",1.0]")
597+
XCTAssertEqual(try trySerializePretty(json), "[\(prettyString),\n\t[\n\t\tnull,\n\t\t\"hello\",\n\t\t1.0\n\t],\n\tnull,\n\t\"hello\",\n\t1.0\n]")
598+
599+
}
600+
601+
// MARK: - Error checkers
602+
func test_invalidJSON() {
603+
let str = "Invalid json"
604+
do {
605+
let _ = try NSJSONSerialization.dataWithJSONObject(str, options: [])
606+
XCTFail("Parsed string to JSON object")
607+
} catch {
608+
}
609+
let doub = 4.0
610+
do {
611+
let _ = try NSJSONSerialization.dataWithJSONObject(doub, options: [])
612+
XCTFail("Parsed double to JSON object")
613+
} catch {
614+
}
615+
struct Temp {
616+
var x : Int
617+
}
618+
let t = Temp(x: 1)
619+
do {
620+
let _ = try NSJSONSerialization.dataWithJSONObject(t, options: [])
621+
XCTFail("Parsed non serializable object to JSON")
622+
} catch {
623+
}
624+
let invalidJSON = ["some": t]
625+
do {
626+
let _ = try NSJSONSerialization.dataWithJSONObject(invalidJSON, options: [])
627+
XCTFail("Parsed non serializable object in dictionary to JSON")
628+
} catch {
629+
}
630+
}
631+
}
632+
455633

456634
// MARK: - isValidJSONObjectTests
457635
extension TestNSJSONSerialization {

0 commit comments

Comments
 (0)