Есть ли в Swift 5 разумный способ разобрать вложенную JSON с неизвестной структурой в объект или словарь? - PullRequest
0 голосов
/ 18 января 2020

Отказ от ответственности: Есть довольно много похожих вопросов, но ни один из них, похоже, не дублирует мой собственный

Предположим, мое приложение получает JSON от довольно плохо спроектированного API, и что JSON, что выглядит примерно так:

{
    "matches": {
        "page1": [{
            "name": "John",
            "surname": "Doe",
            "interests": [{
                    "id": 13,
                    "text": "basketball"
                },
                {
                    "id": 37,
                    "text": "competitive knitting"
                },
                {
                    "id": 127,
                    "text": "romcoms"
                }
            ]
        }],
        "page2": [{
            "name": "Dwayne",
            "surname": "Johnson",
            "interests": [{
                    "id": 42,
                    "text": "sci-fi"
                },
                {
                    "id": 255,
                    "text": "round numbers"
                }
            ]
        }]
    }
}

Если я хочу получить, скажем, все интересы из всех матчей, в нативной функциональности Swift, мне сначала нужно сделать что-то вроде этого:

struct MatchesData: Codable {
    let matches: Matches
}

struct Matches: Codable {
    let page1: Page1
    let page2: Page2
}

struct Page1: Codable {
    let interests: [Interest]
}

struct Page2: Codable {
    let interests: [Interest]
}

struct Interest: Codable {
    let id: Int
    let text: String
}

Затем я должен был бы использовать созданные мной структуры следующим образом:

func handleJSON(_ response: Data) {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(MatchesData.self, from: response)
            // Only here I can start actually working with the data API sent me, it's in decodedData
            ...
        } catch {
            // Handle the errors somehow
            return
        }
}

Хотя этот своего рода работает, у него есть два основных недостатка.

Во-первых, то, что все вместе, - это безумное количество подготовки и кода в целом для такой простой задачи, и оно не соответствует принципу DRY.

Наконец, такой подход просто не работает, если вы не знаете точную структуру JSON заранее. Например, в моем примере, что, если количество страниц не было установлено равным 2, а могло бы быть где-то между, скажем, 1 и 50?

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

Примеры (без мер безопасности для улучшения читабельности) :

JS:

const handleJSON = jsonStr => {
  const jsonObj = JSON.parse(jsonStr)
  const matches = jsonObj.matches
  Object.values(matches).forEach(page => {
    // Recursively process every page to find what I need and process it
  })
}

Python 3:

import json

def handleJSON(jsonStr):
  jsonObj = json.loads(jsonStr)
  matches = jsonObj['matches']
  for page in matches:
    # Recursively process every page to find what I need and process it

PHP:

function handleJSON($jsonStr) {
    $jsonObj = json_decode($jsonStr);
    $matches = $jsonObj->matches;
    foreach ($matches as $page) {
        // Recursively process every page to find what I need and process it
    }
}

Итак, вопрос в том, как мне добиться того же в Swift в разумной манере, как в примерах выше (что определенно означает примерно одинаковое количество кода)? Если вы знаете стороннюю библиотеку, которая делает именно это, я бы с радостью принял такую ​​библиотеку за ответ.

ОБНОВЛЕНИЕ: Только что узнал о Jsonify , что, по-видимому, является таким же хорошим решением моих проблем, как Swifty JSON, может быть, даже лучше. Вероятно, стоит попробовать какое-то время, чтобы решить, какой из них подходит им по вкусу и нуждается лучше

Ответы [ 2 ]

5 голосов
/ 18 января 2020

Прежде всего, помните, что нет необходимости, чтобы структуры были глобальными по объему. Вы можете определить структуру "временно" внутри функции только для того, чтобы помочь вам погрузиться в JSON и извлечь значение. Таким образом, общая архитектура кода никоим образом не испорчена.

Во-вторых, ваш собственный пример JSON не настолько безумный, как вы предлагаете; что безумно, так это ваши собственные структуры. Во-первых, вы используете две одинаковые структуры. Нет никакой цели иметь структуру Page1 и структуру Page2. Во-вторых, в ваших матчах тоже нет цели. Это не то, что ваш JSON. В ваших MatchesData свойство matches должно быть просто словарем типа [String:[Person]] (у вас нет типа Person, но это то, чем эти вещи кажутся).

Если вы утверждаете, что словарь должен быть массивом в JSON, хорошо, я согласен. Если ключи всегда будут называться "page1", "page2", et c., То JSON глупо в этом отношении. Но тогда вы можете позже преобразовать [String:[Person]] в массив Person, упорядоченный по номерам на конце ключей String "page". И затем вы можете отобразить это в массив интересов, если вы хотите выбросить остальную информацию о человеке.

Другими словами: как вы анализируете данные, которые приходят к вам, и то, как вы сохраняете данные, которые вас интересуют, - это два совершенно разных вопроса. Разбирайте данные, просто выведите себя из мира JSON в объектный мир; затем преобразуйте его в форму, которая будет вам полезна, сохраняя то, что вам нужно, и игнорируя то, что вы не делаете.

Вот пример того, что вы можете сделать:

let data = s.data(using: .utf8)!

struct Result : Codable {
    let matches:[String:[Person]]
}
struct Person : Codable {
    let name:String
    let surname:String
    let interests:[Interests]
}
struct Interests : Codable {
    let id:Int
    let text:String
}
let result = try! JSONDecoder().decode(Result.self, from: data)

Хорошо, теперь мы проанализировали данные. Но мы ненавидим его структуру. Так что измени это!

// okay, but dictionary is silly: flatten to an array
let persons1 = Array(result.matches)
func pageNameToNumber(_ s:String) -> Int {
    let num = s.dropFirst(4)
    return Int(num)!
}
let persons2 = persons1.map {(key:pageNameToNumber($0.key), value:$0.value)}
let persons3 = persons2.sorted {$0.key < $1.key}
let persons4 = persons3.map { $0.value }

Но у нас все еще есть глупые одноэлементные массивы, с которыми приходится иметь дело:

// okay, but the one-element array is silly, so flatten that too
let persons5 = persons4.map {$0.first!}

И теперь ты говоришь, что никогда не заботился о людях, вообще все, что вам нужно, это список интересов:

// okay but all I care about are the interests
let interests = persons5.map {$0.interests}

(И тогда вы могли бы также сгладить интересы или что-то еще.)

Теперь, если все это было сделано в методе тогда только структура интересов должна быть общедоступной; все остальное - всего лишь инструмент для извлечения значений и может быть закрытым для внутренней части метода. Только опубликуйте c структуры, которые действительно необходимы / нужны вашему приложению для обслуживания данных. Общий урок, однако, заключается в следующем: просто разберите проклятый JSON на объекты: теперь вы находитесь в объектном мире Swift и можете повторно анализировать эти объекты любым удобным для вас способом.

1 голос
/ 18 января 2020

Если вы ищете внешнюю библиотеку, которая делает это, вы можете попробовать Swifty JSON, которая позволяет вам получить все интересы, как это:

    let json = JSON(parseJSON: jsonString)
    let matchesObject = json["matches"]
    // I assume the total number of pages is stored in numberOfPages
    let interests = (0..<2).flatMap {
        matchesObject["page\($0 + 1)"].arrayValue.flatMap {
            $0["interests"].arrayValue.map {
                // using the same Interest struct as in your question
                Interest(id: $0["id"].intValue, text: $0["text"].stringValue)
            }
        }
    }
...