Skip to content

Added JSON serialization mechanism #137

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 117 additions & 2 deletions Foundation/NSJSONSerialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ public class NSJSONSerialization : NSObject {

/* 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.
*/
public class func dataWithJSONObject(obj: AnyObject, options opt: NSJSONWritingOptions) throws -> NSData {
NSUnimplemented()
public class func dataWithJSONObject(obj: Any, options opt: NSJSONWritingOptions) throws -> NSData {
let (newIndent, newLine, spacing) = opt.contains(NSJSONWritingOptions.PrettyPrinted) ? ("\t", "\n", " ") : ("", "", "")
let str = NSString(try JSONSerialize(obj, newIndent: newIndent, newLine: newLine, space: spacing, topLevel: true))
return str.dataUsingEncoding(NSUTF8StringEncoding)!
}

/* 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.
Expand Down Expand Up @@ -147,6 +149,119 @@ public class NSJSONSerialization : NSObject {
}
}

// MARK: - Serialization
private extension NSJSONSerialization {

private class func escapeStringForJSON(string: String) -> String {
return NSString(NSString(string).stringByReplacingOccurrencesOfString("\\", withString: "\\\\")).stringByReplacingOccurrencesOfString("\"", withString: "\\\"")
}
private class func serializeArray<T>(array: [T], newIndent: String, newLine: String, space: String, indentation: String) throws -> String {
guard array.count > 0
else {
return "\(indentation)[]"
}
var index = 0
return try array.reduce("\(indentation)[\(newLine)", combine: {
index += 1
let separator = (index == array.count) ? "\(newLine)\(indentation)]" : ",\(newLine)"
return $0 + (try JSONSerialize($1, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation + newIndent)) + separator
})
}

private class func serializePOD<T: Any>(object: T, indentation: String) throws -> String {
if let str = object as? String {
return indentation + "\"\(escapeStringForJSON(str))\""
} else if let _ = object as? NSNull {
return indentation + "null"
} else if let obj = object as? CustomStringConvertible {
// Using CustomStringConvertible is a hack to allow for all POD types
// Once bridging works we can revert back.
// TODO: Fix when bridging works
return indentation + escapeStringForJSON(obj.description)
}
else if let num = object as? NSNumber {
return num.description
} else {
throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListWriteInvalidError.rawValue, userInfo: [
"NSDebugDescription" : "Cannot serialize \(object.dynamicType)"
])
}
}

private class func serializeObject<T>(object: [String: T], newIndent: String, newLine: String, space: String, indentation: String) throws -> String {
guard object.count > 0
else {
return "\(indentation){}"
}
var index = 0
return try object.reduce("\(indentation){\(newLine)", combine: {
let valueString : String
do {
valueString = try serializePOD($1.1, indentation: "")
}
catch {
valueString = try JSONSerialize($1.1, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation + newIndent)
}
index += 1
let separator = (index == object.count) ? "\(newLine)\(indentation)}" : ",\(newLine)"
return $0 + "\(indentation + newIndent)\"" + escapeStringForJSON($1.0) + "\"\(space):\(space)" + valueString + separator
})
}

private class func JSONSerialize<T>(object: T, newIndent: String, newLine: String, space: String, topLevel: Bool = false, indentation: String = "") throws -> String {
// TODO: - revisit this once bridging story gets fully figured out
if let array = object as? [Any] {
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let array = object as? NSArray {
return try serializeArray(array.bridge(), newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let array = object as? [NSNumber] {
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let array = object as? [String] {
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let array = object as? [Double] {
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let array = object as? [Int] {
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let array = object as? [Bool] {
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let array = object as? [Float] {
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let array = object as? [UInt] {
return try serializeArray(array, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let dict = object as? [String : Any] {
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let dict = object as? NSDictionary {
return try JSONSerialize(dict.bridge(), newIndent: newIndent, newLine: newLine, space: space, topLevel: topLevel, indentation: indentation)
} else if let dict = object as? [String: String] {
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let dict = object as? [String: Double] {
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let dict = object as? [String: Int] {
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let dict = object as? [String: Bool] {
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let dict = object as? [String: Float] {
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let dict = object as? [String: UInt] {
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
} else if let dict = object as? [String: NSNumber] {
return try serializeObject(dict, newIndent: newIndent, newLine: newLine, space: space, indentation: indentation)
}
// This else if situation will greatly improve once bridging works properly.
// For now we check for Floats, Doubles, Int, UInt, Bool and NSNumber
// but this may disallow other types like CGFloat, Int32, etc which is a problem
else {
guard !topLevel
else {
throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListWriteInvalidError.rawValue, userInfo: [
"NSDebugDescription" : "Unable to use \(object.dynamicType) as a top level JSON object."
])
}
return try serializePOD(object, indentation: indentation)
}
}
}

//MARK: - Encoding Detection

internal extension NSJSONSerialization {
Expand Down
178 changes: 178 additions & 0 deletions TestFoundation/TestNSJSONSerialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class TestNSJSONSerialization : XCTestCase {
static var allTests: [(String, TestNSJSONSerialization -> () throws -> Void)] {
return JSONObjectWithDataTests
+ deserializationTests
+ serializationTests
+ isValidJSONObjectTests
}

Expand Down Expand Up @@ -452,6 +453,183 @@ extension TestNSJSONSerialization {
}

}
// MARK: - JSONSerialization
extension TestNSJSONSerialization {

class var serializationTests: [(String, TestNSJSONSerialization -> () throws -> Void)] {
return [
("test_serialize_emptyObject", test_serialize_emptyObject),
("test_serialize_multiStringObject", test_serialize_multiStringObject),
("test_serialize_complicatedObject", test_serialize_complicatedObject),

("test_serialize_emptyArray", test_serialize_emptyArray),
("test_serialize_multiPODArray", test_serialize_multiPODArray),

("test_serialize_nested", test_serialize_nested),
("test_invalid_json", test_invalidJSON)
]
}

func trySerializeHelper(subject: Any, options: NSJSONWritingOptions) throws -> String {
let data = try NSJSONSerialization.dataWithJSONObject(subject, options: options)
guard let string = NSString(data: data, encoding: NSUTF8StringEncoding)
else {
XCTFail("Unable to convert data to string")
return ""
}
return string.bridge()
}
func trySerialize(subject: Any) throws -> String {
return try trySerializeHelper(subject, options: [])
}
func trySerializePretty(subject: Any) throws -> String {
return try trySerializeHelper(subject, options: [.PrettyPrinted])
}

//MARK: - Object Serialization
func test_serialize_emptyObject() {

XCTAssertEqual(try trySerialize([String: Any]()), "{}")
XCTAssertEqual(try trySerializePretty([String: Any]()), "{}")

XCTAssertEqual(try trySerialize([String: NSNumber]()), "{}")
XCTAssertEqual(try trySerialize([String: String]()), "{}")

var json = [String: Double]()
json["s"] = 1.0
json["s"] = nil
XCTAssertEqual(try trySerialize(json), "{}")
XCTAssertEqual(try trySerializePretty(json), "{}")
}
func test_serialize_multiStringObject() {

var json = [String: String]()
json["hello"] = "world"
XCTAssertEqual(try trySerialize(json), "{\"hello\":\"world\"}")
XCTAssertEqual(try trySerializePretty(json), "{\n\t\"hello\" : \"world\"\n}")

// testing escaped characters like " and \
json["swift"] = "is \\ \"awesome\""
XCTAssertEqual(try trySerialize(json), "{\"hello\":\"world\",\"swift\":\"is \\\\ \\\"awesome\\\"\"}")
XCTAssertEqual(try trySerializePretty(json), "{\n\t\"hello\" : \"world\",\n\t\"swift\" : \"is \\\\ \\\"awesome\\\"\"\n}")
}
func test_serialize_complicatedObject() {
let json : [String: Any] = ["a": 4.0, "b": NSNull(), "c": "string", "d": false]

var normalString = "{"
var prettyString = "{"
for key in json.keys {
normalString += "\"\(key)\":"
prettyString += "\n\t\"\(key)\" : "
let valueString : String
if key == "a" {
valueString = "4.0"
} else if key == "b" {
valueString = "null"
} else if key == "c" {
valueString = "\"string\""
} else {
valueString = "false"
}
normalString += "\(valueString),"
prettyString += "\(valueString),"
}
normalString = normalString.substringToIndex(normalString.endIndex.predecessor()) + "}"
prettyString = prettyString.substringToIndex(prettyString.endIndex.predecessor()) + "\n}"
XCTAssertEqual(try trySerialize(json), normalString)
XCTAssertEqual(try trySerializePretty(json), prettyString)
}

//MARK: - Array Serialization
func test_serialize_emptyArray() {

XCTAssertEqual(try trySerialize([String]()), "[]")
XCTAssertEqual(try trySerializePretty([String]()), "[]")

XCTAssertEqual(try trySerialize([NSNumber]()), "[]")
XCTAssertEqual(try trySerializePretty([NSNumber]()), "[]")

XCTAssertEqual(try trySerialize([Any]()), "[]")
}
func test_serialize_multiPODArray() {
XCTAssertEqual(try trySerialize(["hello", "swift⚡️"]), "[\"hello\",\"swift⚡️\"]")
XCTAssertEqual(try trySerializePretty(["hello", "swift⚡️"]), "[\n\t\"hello\",\n\t\"swift⚡️\"\n]")

XCTAssertEqual(try trySerialize([1.0, 3.3, 2.3]), "[1.0,3.3,2.3]")
XCTAssertEqual(try trySerializePretty([1.0, 3.3, 2.3]), "[\n\t1.0,\n\t3.3,\n\t2.3\n]")


let array: [Any] = [NSNull(), "hello", 1.0]
XCTAssertEqual(try trySerialize(array), "[null,\"hello\",1.0]")
XCTAssertEqual(try trySerializePretty(array), "[\n\tnull,\n\t\"hello\",\n\t1.0\n]")
}

//MARK: - Nested Serialization
func test_serialize_nested() {
let first: [String: Any] = ["a": 4.0, "b": NSNull(), "c": "string", "d": false]

var normalString = "{"
var prettyString = "\n\t{"

for key in first.keys {
normalString += "\"\(key)\":"
prettyString += "\n\t\t\"\(key)\" : "
let valueString : String
if key == "a" {
valueString = "4.0"
} else if key == "b" {
valueString = "null"
} else if key == "c" {
valueString = "\"string\""
} else {
valueString = "false"
}
normalString += "\(valueString),"
prettyString += "\(valueString),"
}
normalString = normalString.substringToIndex(normalString.endIndex.predecessor()) + "}"
prettyString = prettyString.substringToIndex(prettyString.endIndex.predecessor()) + "\n\t}"

let second: [Any] = [NSNull(), "hello", 1.0]
let json: [Any] = [first, second, NSNull(), "hello", 1.0]

XCTAssertEqual(try trySerialize(json), "[\(normalString),[null,\"hello\",1.0],null,\"hello\",1.0]")
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]")

}

// MARK: - Error checkers
func test_invalidJSON() {
let str = "Invalid json"
do {
let _ = try NSJSONSerialization.dataWithJSONObject(str, options: [])
XCTFail("Parsed string to JSON object")
} catch {
}
let doub = 4.0
do {
let _ = try NSJSONSerialization.dataWithJSONObject(doub, options: [])
XCTFail("Parsed double to JSON object")
} catch {
}
struct Temp {
var x : Int
}
let t = Temp(x: 1)
do {
let _ = try NSJSONSerialization.dataWithJSONObject(t, options: [])
XCTFail("Parsed non serializable object to JSON")
} catch {
}
let invalidJSON = ["some": t]
do {
let _ = try NSJSONSerialization.dataWithJSONObject(invalidJSON, options: [])
XCTFail("Parsed non serializable object in dictionary to JSON")
} catch {
}
}
}


// MARK: - isValidJSONObjectTests
extension TestNSJSONSerialization {
Expand Down