将JSON或其他无类型数据转换为Swift类型类的最佳方法?

时间:2014-06-16 01:38:08

标签: json swift

我正在尝试将JSON解析为类型类以确保安全/方便,但它证明非常笨重。我无法找到一个图书馆,甚至找不到Swift的帖子(Jastor和我一样接近)。这是一个虚构的小片段来说明:

// From NSJSONSerialization or similar and casted to an appropriate toplevel type (e.g. Dictionary). 
var parsedJson: Dictionary<String, AnyObject> = [ "int" : 1, "nested" : [ "bool" : true ] ]

class TypedObject {
    let stringValueWithDefault: String = ""
    let intValueRequired: Int
    let nestedBoolBroughtToTopLevel: Bool = false
    let combinedIntRequired: Int

    init(fromParsedJson json: NSDictionary) {
        if let parsedStringValue = json["string"] as? String {
            self.stringValueWithDefault = parsedStringValue
        }

        if let parsedIntValue = json["int"] as? Int {
            self.intValueRequired = parsedIntValue
        } else {
            // Raise an exception...?
        }

        // Optional-chaining is actually pretty nice for this; it keeps the blocks from nesting absurdly.
        if let parsedBool = json["nested"]?["bool"] as? Bool {
            self.nestedBoolBroughtToTopLevel = parsedBool
        }

        if let parsedFirstInt = json["firstInt"] as? Int {
            if let parsedSecondInt = json["secondInt"] as? Int {
                self.combinedIntRequired = parsedFirstInt * parsedSecondInt
            }
        }
        // Most succinct way to error if we weren't able to construct self.combinedIntRequired?
    }
}

TypedObject(fromParsedJson: parsedJson)

这里有很多问题,我希望能解决这个问题:

  1. 这是非常冗长的,因为为了安全起见,我需要在复制粘贴的if - let中包装每个属性。
  2. 我不确定在缺少必需属性时如何传达错误(如上所述)。 Swift似乎更喜欢(?)使用例外来显示停止问题(而不是像这里的行人格式错误的数据)。
  3. 我不知道处理存在的属性但是类型错误的好方法(假设as?转换将失败并且只是跳过块,它对用户来说不是很有用)。< / LI>
  4. 如果我想将一些属性转换为单个属性,我需要将let块与我正在组合的属性数量成比例。 (这可能更普遍地是将多个选项安全地组合成一个值的问题。)
  5. 一般情况下,当我觉得我应该能够做一些更具声明性的事情时(或者使用一些陈述的JSON模式或者至少从类定义中推断出模式),我正在编写命令式解析逻辑。

3 个答案:

答案 0 :(得分:0)

我使用Jastor框架执行此操作:

1)实现一个具有单个函数的协议,该函数返回NSDictionary响应:

protocol APIProtocol {
    func didReceiveResponse(results: NSDictionary)
}

2)创建一个定义NSURLConnection对象的API类,该对象可用作iOS网络API的请求URL。创建此类只是为了从itunes.apple.com API返回有效负载。

class API: NSObject {

    var data: NSMutableData = NSMutableData()
    var delegate: APIProtocol?

    func searchItunesFor(searchTerm: String) {

        // Clean up the search terms by replacing spaces with +
        var itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+",
            options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)

        var escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
        var urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music"
        var url: NSURL = NSURL(string: urlPath)
        var request: NSURLRequest = NSURLRequest(URL: url)
        var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)

        println("Search iTunes API at URL \(url)")

        connection.start()
    }

    // NSURLConnection Connection failed.
    func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
        println("Failed with error:\(error.localizedDescription)")
    }

    // New request so we need to clear the data object.
    func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
        self.data = NSMutableData()
    }

    // Append incoming data.
    func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
        self.data.appendData(data)
    }

    // NSURLConnection delegate function.
    func connectionDidFinishLoading(connection: NSURLConnection!) {

        // Finished receiving data and convert it to a JSON object.
        var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data,
            options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary

        delegate?.didReceiveResponse(jsonResult)
    }

}

3)创建一个具有从Jastor

继承的关联属性的类

NSDictionary响应:

{
  "resultCount" : 50,
  "results" : [
    {
      "collectionExplicitness" : "notExplicit",
      "discCount" : 1,
      "artworkUrl60" : "http:\/\/a4.mzstatic.com\/us\/r30\/Features\/2a\/b7\/da\/dj.kkirmfzh.60x60-50.jpg",
      "collectionCensoredName" : "Changes in Latitudes, Changes in Attitudes (Ultmate Master Disk  Gold CD  Reissue)"
    }
   ]
}

<强> Music.swift

class Music : Jastor {

    var resultCount: NSNumber = 0

}

4)然后在ViewController中确保将委托设置为self,然后调用API的searchITunesFor()方法。

var api: API = API()

override func viewDidLoad() {

    api.delegate = self;
    api.searchItunesFor("Led Zeppelin")

}

5)为didReceiveResponse()实施Delegate方法。 Jastor扩展您的类以设置从iTunes API返回的结果的NSDictionary

// #pragma - API Delegates

func didReceiveResponse(results: NSDictionary) {
    let music = Music(dictionary: results)
    println(music)
}

答案 1 :(得分:0)

简短版本:由于init不允许失败,因此必须在其外部进行验证。在这些情况下,可选项似乎是流量控制的预期工具。我的解决方案是使用一个返回类的可选项的工厂方法,并在其中使用选项链接来提取和验证字段。

另请注意IntBool不是AnyObject的孩子;来自NSDictionary的数据会将它们存储为NSNumber s,而这些数据不能直接转换为Swift类型。因此,调用.integerValue.boolValue

长版:

// Start with NSDictionary since that's what NSJSONSerialization will give us
var invalidJson: NSDictionary = [ "int" : 1, "nested" : [ "bool" : true ] ]

var validJson: NSDictionary = [
    "int" : 1,
    "nested" : [ "bool" : true ],
    "firstInt" : 3,
    "secondInt" : 5
]


class TypedObject {

    let stringValueWithDefault: String = ""
    let intValueRequired: Int
    let nestedBoolBroughtToTopLevel: Bool = false
    let combinedIntRequired: Int

    init(intValue: Int, combinedInt: Int, stringValue: String?, nestedBool: Bool?) {

        self.intValueRequired = intValue
        self.combinedIntRequired = combinedInt

        // Use Optionals for the non-required parameters so
        // we know whether to leave the default values in place
        if let s = stringValue {
            self.stringValueWithDefault = s
        }

        if let n = nestedBool {
            self.nestedBoolBroughtToTopLevel = n
        }
    }

    class func createFromDictionary(json: Dictionary<String, AnyObject>) -> TypedObject? {

        // Validate required fields
        var intValue: Int

        if let x = (json["int"]? as? NSNumber)?.integerValue {
            intValue = x
        } else {
            return nil
        }


        var combinedInt: Int

        let firstInt = (json["firstInt"]? as? NSNumber)?.integerValue
        let secondInt = (json["secondInt"]? as? NSNumber)?.integerValue

        switch (firstInt, secondInt) {
        case (.Some(let first), .Some(let second)):
            combinedInt = first * second
        default:
            return nil
        }

        // Extract optional fields
        // For some reason the compiler didn't like casting from AnyObject to String directly
        let stringValue = json["string"]? as? NSString as? String
        let nestedBool = (json["nested"]?["bool"]? as? NSNumber)?.boolValue

        return TypedObject(intValue: intValue, combinedInt: combinedInt, stringValue: stringValue, nestedBool: nestedBool)
    }

    class func createFromDictionary(json: NSDictionary) -> TypedObject? {
        // Manually doing this cast since it works, and the only thing Apple's docs
        // currently say about bridging Cocoa and Dictionaries is "Information forthcoming"
        return TypedObject.createFromDictionary(json as Dictionary<String, AnyObject>)
    }
}

TypedObject.createFromDictionary(invalidJson) // nil
TypedObject.createFromDictionary(validJson) // it works!

答案 2 :(得分:0)

我还完成了以下转换为/来自:

class Image {
    var _id = String()
    var title = String()
    var subTitle = String()
    var imageId = String()

    func toDictionary(dict dictionary: NSDictionary) {
        self._id = dictionary["_id"] as String
        self.title = dictionary["title"] as String
        self.subTitle = dictionary["subTitle"] as String
        self.imageId = dictionary["imageId"] as String
    }

    func safeSet(d: NSMutableDictionary, k: String, v: String) {
        if (v != nil) {
            d[k] = v
        }
    }

    func toDictionary() -> NSDictionary {
        let jsonable = NSMutableDictionary()
        self.safeSet(jsonable, k: "title", v: self.title);
        self.safeSet(jsonable, k: "subTitle", v: self.subTitle);
        self.safeSet(jsonable, k: "imageId", v: self.imageId);

        return jsonable
    }
}

然后我简单地执行以下操作:

// data (from service)
let responseArray = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: nil) as NSArray

self.objects = NSMutableArray()
for item: AnyObject in responseArray {
    var image = Image()
    image.toDictionary(dict: item as NSDictionary)
    self.objects.addObject(image)
}

如果您想发布数据:

var image = Image()
image.title = "title"
image.subTitle = "subTitle"
image.imageId = "imageId"

let data = NSJSONSerialization.dataWithJSONObject(image.toDictionary(), options: .PrettyPrinted, error: nil) as NSData

// data (to service)
request.HTTPBody = data;