Работа с JSON — слишком привычное и ежедневное занятие, чтобы уделять ей много внимания. Тем не менее, реализация некоторых вещей в Swift выглядит слишком сложной и вызывает зубовную боль каждый раз, когда ее видишь.

Недавно, читая пост про SwiftyVK, нашел там ссылку на статью про OptJSON, позволяющую сильно упростить работу с JSON в Swift. И хотя подход, описанный в статье, действительно интересен, меня не покидало ощущение, что это все-равно слишком сложно.

Я попробовал еще немного упростить библиотеку OptJSON, и вот что получилось:

let obj = json?["workplan"]?["presets"]?[1]?["id"] as? Int

Исходный код библиотеки можно посмотреть по ссылке на GitHub:

Скачав единственный файл OptJSON.swift, я добавил его в пустой, только что созданный тестовый проект под Apple TV. Xcode ругнулся, что версия Swift в файле слишком старая и предложил обновить код. Я не стал возражать. По факту исправления коснулись лишь удаления хэш-символов (#).

Также я включил в проект JSON-конфиг, который использовал ранее в другом проекте и попробовал написать тестовый код для извлечения какого-нибудь значения «по-старинке»:

if let data = NSData(contentsOfFile: NSBundle.mainBundle().pathForResource("config", ofType: "json")!) {
  do {
    let obj = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject]
    let a = obj?["workplan"] as? [String: AnyObject]
    let b = a?["presets"] as? [AnyObject]
    print(b)
    let c = b?[1] as? [String: AnyObject]
    print(c)
    let d = c?["id"] as? Int
    print(d)
  } catch {
    print("Error!")
  }
}

Да, в Objective C действительно было в разы проще. Теперь попробуем использовать OptJSON для тех же целей:

Если длинно
if let data = NSData(contentsOfFile: NSBundle.mainBundle().pathForResource("config", ofType: "json")!) {
  do {
    let obj = try NSJSONSerialization.JSONObjectWithData(data, options: [])
     let v = JSON(obj)
     let a = v?[key:"workplan"]
     let b = a?[key:"presets"]
     print(b)
     let c = b?[index:1]
     print(c)
     let d = c?[key:"id"]
     print(d)
  } catch {
     print("Error!")
  }
}


Или если коротко:

if let data = NSData(contentsOfFile: NSBundle.mainBundle().pathForResource("config", ofType: "json")!) {
  do {
    let obj = try NSJSONSerialization.JSONObjectWithData(data, options: [])
    let d = JSON(obj)?[key:"workplan"]?[key:"presets"]?[index:1]?[key:"id"]
    print(d)
  } catch {
    print("Error!")
  }
}

Уже неплохо! Но все равно, не покидает ощущение, что можно все сделать проще. И ведь можно! Я полез в OptJSON.swift и, первым делом, меня удивила конструкция:

public func JSON(object: AnyObject?) -> JSONValue? {
  if let some: AnyObject = object {
    switch some {
     case let null as NSNull:        return null
     case let number as NSNumber:    return number
     case let string as NSString:    return string
     case let array as NSArray:      return array
     case let dict as NSDictionary:  return dict
     default:                        return nil
    }
  } else {
     return nil
  }
}

Не долго думая, я ее заменил на

public func JSONValue(object: AnyObject?) -> JSONValue? {
  if let some = object as? JSONValue {
    return some
  } else {
    return nil
  }
}

А если нет разницы, зачем платить больше? Следующим шагом было убрать именованные параметры, так сильно бесящие при вызове subscript:

[key:"presets"]?[index:0]?

Сказано — сделано! Xcode ругнулся, что ему не нравится возвращаемый тип JSONValue? вместо ожидаемого subscript-ом AnyObject?.. Чтож, попутно сносим обертку возвращаемых значений в JSON(). Код обрел примерно следующий вид:

extension NSArray : JSONValue {
  public subscript( key: String) -> JSONValue? { return nil }
  public subscript( index: Int) -> JSONValue? { return index < count && index >= 0 ? self[index] : nil }
}

extension NSDictionary : JSONValue {
  public subscript( key: String) -> JSONValue? { return self[key] }
  public subscript( index: Int) -> JSONValue? { return nil }
}

Запустив проект, я понял, почему автор решил использовать именованные параметры, а именно, выполнение функции уходило в глубокую рекурсию, пытаясь вызвать self[key]. Но в конце концов, почему для расширения используются классы NSArray и NSDictionary, а для извлечения объекта — чуждый objectForKeyedSubscript?! Ведь есть же родные для этих классов методы objectForKey: и objectAtIndex:, используем их:

extension NSArray : JSONValue {
  public subscript( key: String) -> JSONValue? { return nil }
  public subscript( index: Int) -> JSONValue? { return index < count && index >= 0 ? self.objectAtIndex(index) as? JSONValue : nil }
}

extension NSDictionary : JSONValue {
  public subscript( key: String) -> JSONValue? { return self.objectForKey(key) as? JSONValue }
  public subscript( index: Int) -> JSONValue? { return nil }
}

А раз пошла такая пьянка, то функция JSON() для обертки объектов нам в принципе не нужна, хватит и простого as? JSONValue. Заменим ее внутренности для чего-нибудь более удобного, например загрузка JSON объекта из строки, NSData или из содержимого NSURL, а заодно избавимся от необходимости использования новомодного try-catch:

public func JSON(object: AnyObject?, options: NSJSONReadingOptions = []) -> JSONValue? {
  let data: NSData
  if let aData = object as? NSData {
    data = aData
  } else if let string = object as? String, aData = string.dataUsingEncoding(NSUTF8StringEncoding) {
    data = aData
  } else if let url = object as? NSURL, aData = NSData(contentsOfURL: url) {
    data = aData
  } else {
    return nil
  }
  if let json = try? NSJSONSerialization.JSONObjectWithData(data, options: options) {
    return json as? JSONValue
  }
  return nil
}

После этих преобразований, конечный код приобретает следующий вид:

if let v = JSON(NSBundle.mainBundle().URLForResource("config", withExtension: "json")) {
  let a = v["workplan"]
  let b = a?["presets"]
  print(b)
  let c = b?[1]
  print(c)
  let d = c?["id"]
  print(d)
}

А если короче, то и вовсе:

let json = JSON(NSBundle.mainBundle().URLForResource("config", withExtension: "json"))
let obj = json?["workplan"]?["presets"]?[1]?["id"] as? Int
print(obj)

И никаких именованных параметров! Спасибо за внимание.

Просмотреть полный код доработанного файла
import Foundation

public protocol JSONValue: AnyObject {
    subscript(key: String) -> JSONValue? { get }
    subscript(index: Int) -> JSONValue? { get }
}

extension NSNull : JSONValue {
    public subscript(key: String) -> JSONValue? { return nil }
    public subscript(index: Int) -> JSONValue? { return nil }
}

extension NSNumber : JSONValue {
    public subscript(key: String) -> JSONValue? { return nil }
    public subscript(index: Int) -> JSONValue? { return nil }
}

extension NSString : JSONValue {
    public subscript( key: String) -> JSONValue? { return nil }
    public subscript( index: Int) -> JSONValue? { return nil }
}

extension NSArray : JSONValue {
    public subscript( key: String) -> JSONValue? { return nil }
    public subscript( index: Int) -> JSONValue? { return index < count && index >= 0 ? self.objectAtIndex(index) as? JSONValue : nil }
}

extension NSDictionary : JSONValue {
    public subscript( key: String) -> JSONValue? { return self.objectForKey(key) as? JSONValue }
    public subscript( index: Int) -> JSONValue? { return nil }
}


public func JSON(object: AnyObject?, options: NSJSONReadingOptions = []) -> JSONValue? {
    let data: NSData
    if let aData = object as? NSData {
        data = aData
    } else if let string = object as? String, aData = string.dataUsingEncoding(NSUTF8StringEncoding) {
        data = aData
    } else if let url = object as? NSURL, aData = NSData(contentsOfURL: url) {
        data = aData
    } else {
        return nil
    }
    if let json = try? NSJSONSerialization.JSONObjectWithData(data, options: options) {
        return json as? JSONValue
    }
    return nil
}

Комментарии (8)


  1. freeOne
    03.11.2015 14:28

    Синтаксис страшненький на мой взгляд.

    let obj = json.get("workplan.presets.1.id") as? Int
    

    смотрелось бы не так вырвиглазно


    1. mallexxx
      03.11.2015 14:54
      +2

      Так тоже можно, более того, до этого у меня так и было сделано (правда в ObjC классе).
      Но при этом, во-первых нужно создавать специальный объект для работы с Dictionary, во вторых не очень красива конструкция «presets.1» (можно представить ситуацию, когда неправильно сформированный JSON имеет строковый ключ «1» вместо извлечения по индексу).
      Кроме того, в реальной жизни эта конструкция будет выглядеть немного страшнее: «workplan.presets.\(index).id». Ну и лучше-бы на мой взгляд по subscript извлекать, а не методом .get()
      А если захочется все-таки вытащить Dictionary preset, а потом из него уже все мапить: json.get(«workplan.presets.1»)? Получается дальше уже не получится так красиво, либо еще раз его придется заворачивать во что-то, либо функцией Get возвращать сразу dict в обертке, но это уже слишком :)


  1. WEStor
    03.11.2015 22:49
    +1

    Я пытался работать с optJSON и другие решения пробовал, но все же перешел на SwiftyJSON. Как и писал в статье. Там и синтаксис очень удобный, и библиотека хорошо тестами покрыта, и разработка постоянно продолжается.)

    Вы, конечно, постарались. Если проект будет развиваться, то возьму его на заметку)


    1. Shannon
      03.11.2015 23:15

      SwiftyJSON более прожорлив к процессору (и расходу батареи), у меня в то время когда SwiftyJSON жрал 100% проца и интерфейс тормозил, OptJSON использовал всего 30%
      На небольших данных это не так заметно, но всё же стоит иметь ввиду

      Эта упрощенная версия OptJSON очень порадовала


  1. WEStor
    03.11.2015 22:55

    Можно еще сделать расширение протокола JSONValue чтобы возвращал разные там intValue, stringValue без as. Вообще в Swift 2 все намного проще и интрересней, чем было раньше. Тогда ограничений было намного боьльше. Возможно, поэтому автор и использовал некоторые костыльные решения. Я и в своем коде до сих пор их нахожу и исправляю на более нормальные варианты swift 2.


  1. Shannon
    03.11.2015 23:04

    Именно этого не хватало в optJSON, а точнее именно это и мешало, так что результат отличный


  1. DenHeadless
    05.11.2015 10:42
    -1

    Попробуйте SwiftyJSON — библиотека написана намного более качественно, и код парсинга сокращает в разы.


    1. WEStor
      06.11.2015 09:59

      Про нее уже писали. Более ресурсозатратная либа.