ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Swift) json 다루기
    Swift 2021. 6. 16. 15:00

    네트워크 통신 표준 포맷

    swift의 Dictionary와 유사한 구조

     

    left curly brackets( '{' )로 시작 하고 right curly brackets( '}' )로 짝을 지어 끝난다.

     

    key와 value로 구성

     

    key는 문자열

    value는 숫자, 문자열

     

    []로 배열 표현

     

    Encoding

    기본

    인스턴스를 json 형식으로 인코딩 하는 것.

    구조체나 클래스의 인스턴스를 encoding해보자.

     

    struct Person {
       var firstName: String
       var lastName: String
       var birthDate: Date
       var address: String?
    }
    
    let p = Person(firstName: "Chul_Su", lastName: "Kim", birthDate: Date(timeIntervalSince1970: 1234567), ad

    위의 Person 구조체 인스턴스를 Json 형식으로 인코딩한다.

     

    swift에서 인코딩은 제공되는 JsonEncoder()를 사용한다.

    json으로 인코딩 되는 형식은 반드시 'Encodable' 프로토콜을 채용해야 한다.

    디코딩 또한 형식이 'Decodable' 프로토콜을 채택해야 한다.

    둘다 가능하다면 'Codable' 프로토콜을 채용한다.

    // Codable 프로토콜을 채용한 Person 구조체
    
    struct Person: Codable {
       var firstName: String
       var lastName: String
       var birthDate: Date
       var address: String?
    }

     

    encoding은 위에서 말한 JsonEncoder가 처리해준다. Encoder 객체를 생성해보자.

     

    let encoder = JSONEncoder()

    요놈이 encoding에 관한 모든 것을 처리해준다.

    encoder는 객체 저장되어 있는 데이터를 json 형식(문자열)으로 변환시켜준다.

     

    변환 시켜보자.

     

    do {
        let jsonData = try encoder.encode(p)
        if let jsonStr = String(data: jsonData, encoding: .utf8) {
            print(jsonStr)
        }
    } catch {
        print(error)
    }

     

    인코딩 후의 데이터 = jsonData는 바이너리 데이터이다. 보통 서버와 통신을 주고 받을때는 바이너리 데이터로 진행 하지만, 결과를 보기 위해 Data를 String으로 까보자.

     

    결과

    {"firstName":"Chul_Su","lastName":"Kim","address":"Seoul","birthDate":-977072633}
    

    json 형식의 문자열이 출력된다.

    내가 아는 json은 줄도 나누어져 있고 들여쓰기도 되있던데...

     

    우리의 JSONEncoder는 다 해준다. 

    이쁘게 만들어보자.

    encoder.outputFormatting = .prettyPrinted // 들여쓰기
    
    encoder.outputFormatting = .sortedKeys // 정렬

    outputFormatting을 통해 key의 사전순 정렬과 들여쓰기가 가능하다.

    {
      "firstName" : "Chul_Su",
      "lastName" : "Kim",
      "address" : "Seoul",
      "birthDate" : -977072633
    }

    내가 아는 json 형식이다.

     

     

    Key Encoding Strategy

    JSONEncoder는 속성 이름을 key로 바꿀때 lowerCamel Case를 사용한다.

     

    JSONEncoder의 keyEncodingStrategy를 이용하여 변환 형식을 바꿀 수 있다.

    encoder.keyEncodingStrategy = .convertToSnakeCase // snakeCase 형식
    
    //결과
    
    {
      "address" : "Seoul",
      "last_name" : "Chul-Su",
      "birth_date" : -977072633,
      "firstname" : "Kim"
    }
    

    upper Camel Case로 쓰여진 속성들을 이름 사이에 _를 사용하여 snakeCase로 만들어준다.

     

    Date Encoding Strategy

    날짜 값도 변환해주나?

    ㅇㅇ Encoder는 친절해.

     

    Person의 인스턴스 p를 보면 date값이 timeIntervalSince1970이다.

    1970으로 부터 현재까지의 시간을 초 단위로 나타낸다.

    encoder.dateEncodingStrategy = .iso8601
    
    {
      "firstName" : "Chul-Su",
      "lastName" : "Kim",
      "address" : "Seoul",
      "birthDate" : "1970-01-01T00:00:00Z"
    }
    

    iso8601 표준을 사용하면 위와 같은 형식으로 변환 된다.

     

    DateFormatter를 사용하여 커스텀이 가능하다.

    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-mm-dd"
    encoder.dateEncodingStrategy = .formatted(formatter)
    
    // 결과
    
    {
      "firstName" : "Chul-Su",
      "lastName" : "Kim",
      "address" : "Seoul",
      "birthDate" : "1970-00-01"
    }
    

     

    Decoding

    기본

    이번에는 json에 저장되어 데이터를 person 인스턴스로 디코딩 해보자.

    struct Person: Codable {
       var firstName: String
       var lastName: String
       var age: Int
       var address: String?
    }

     

    let jsonStr = """
    {
    "address" : "Seoul",
    "firstName" : "Chul-Su",
    "age" : 10,
    "lastName" : "Kim"
    
    }
    """
    
    guard let jsonData = jsonStr.data(using: .utf8) else {
       fatalError()
    }

    우선 json 데이터로 변환할 정보를 문자열로 만들고 json data로 만들자.

     

    인코딩과 마찬가지로 디코딩 인스턴스를 생성해주자.

    let decoder = JSONDecoder()

    우리가 원하는 것은 jsondata를 Person 구조체 객체로 변환 하는것이다.

    do {
        let p = try decoder.decode(Person.self, from: jsonData)
        print(p)
    } catch {
        print(error)
    }
    
    // 결과
    Person(firstName: "Chul-Su", lastName: "Kim", age: 10, address: Optional("Seoul"))

    decod 메소드의 파라미터 형식

    decode 메소드의 첫 번째 파라미터는 디코딩할 타입을 전달한다. 타입은 반드시 Decodable 프로토콜을 채택한 형식이거나, Codable을 채용 해야 한다. 두번째 파라미터로는 json 데이터를 전달한다.

    만약 json 데이터가 전달한 type으로 디코딩 할 수 있다면 새로운 Person 객체가 리턴된다.

     

     

     

    위의 경우처럼 json의 key와 형식의 property의 이름이 같고 json의 value와 형식의 property 타입이 같다면 문제가 없겠지만.. 다르다면?

    Key Decoding Strategy

    let jsonStr = """
    {
    "first_name" : "Gil-Dong",
    "age" : 30,
    "last_name" : "Hong",
    "address" : "Seoul"
    }
    """

    위와 같은 snake case의 json 문자열이 있다.

    바로 디코딩 해보자.

    keyNotFound(CodingKeys(stringValue: "firstName", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"firstName\", intValue: nil) (\"firstName\").", underlyingError: nil))
    

    다음과 같이 keyNotFound라는 오류가 뜬다.

    이럴때를 위해 key 디코딩 전략이 존재한다.

     

    기본 전략은 json 형식을 그대로 사용하는 것 이다.

    decoder.keyDecodingStrategy = .convertFromSnakeCase
    
    //결과
    Person(firstName: "Gil-Dong", lastName: "Hong", age: 30, address: Optional("Seoul"))

    converFromSnakeCase의 description을 보면 

    Convert from “snake_case_keys” to “camelCaseKeys” before attempting to match a key with the one specified by each type.

    와 같이 camelCaseKeys로 변환 해준단다.

     

    Date Decoding Strategy

    struct Product: Codable {
       var name: String
       var releaseDate: Date
    }
    
    let jsonStr = """
    {
    "name" : "iPhone X",
    "releaseDate" : "2021-01-20T11:11:11Z"
    }
    """
    
    guard let jsonData = jsonStr.data(using: .utf8) else {
       fatalError()
    }
    
    let decoder = JSONDecoder()
    
    do {
       let p = try decoder.decode(Product.self, from: jsonData)
       dump(p)
    } catch {
       print(error)
    }
    
    
    // 결과
    typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "releaseDate", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil))
    

     

    Product 구조체의 날짜 형식은 Date이다.

    Decoding 과정에서 기본적으로 Date형식은 double값으로 디코딩을 시도한다.

    위의 jsonStr를 Product 타입으로 Decoding 시 releaseDate가 Double형식으로 변환 되지 않기 때문에 오류가 난다.

     

    현재 releaseDate의 형식은 iso8601이다.

    Date Decoding Strategy를 iso8601로 바꿔주자.

    decoder.dateDecodingStrategy = .iso8601
    
    // Product(name: "iPhone X", releaseDate: 2021-01-20 11:11:11 +0000)

     

    Custom Key Mapping

    위의 전략들은 Encoder와 Decoder가 기본으로 제공하는 예외의 경우이다.

    만약 key가 snake case가 아닌 전혀 다른 형식이라면?

    Date형식이 iso8601가 아니라면?

     

    이러한 경우를 위해 Custom Key Mapping이 존재한다.

     

    struct Person: Codable {
        var firstName: String
        var lastName: String
        var age: Int
        var address: String?
    }
    
    let jsonStr = """
    {
    "firstName" : "Gildong",
    "age" : 24,
    "lastName" : "Hong",
    "homeAddress" : "Jeju",
    }
    """
    
    guard let jsonData = jsonStr.data(using: .utf8) else {
        fatalError()
    }
    
    let decoder = JSONDecoder()
    
    do {
        let p = try decoder.decode(Person.self, from: jsonData)
        print(p)
        
    } catch {
        print(error)
    }
    // 결과
    Person(firstName: "Gildong", lastName: "Hong", age: 24, address: nil)
    

     

    Person 구조체의 address 속성은 옵셔널이다.

    따라서 json 데이터와 일치 하지 않아도 nil이 저장된다.

    Custom Key Mapping을 이용하여 homeAddress를 address로 디코딩 해보자.

     

    Key Mapping은 형식 내부에서 열거형으로 선언한다.

    struct Person: Codable {
        var firstName: String
        var lastName: String
        var age: Int
        var address: String?
        
        enum CodingKeys: String, CodingKey { // Key Mapping을 위한 CodingKeys
            case firstName
            case lastName
            case age
            case address = "homeAddress"
        }
    }

     

    Codingkeys는 custom key mapping을 위해 따로 구현하지 않는다면 기본 속성으로 case들이 자동으로 생성된다. 

     

    속성을 mapping 하기위해 case address의 rawValue를 "homeAddress"로 선언 해준다.

    이렇게 하면 address속성과 homeAddress key가 mapping 된다.

     

    Person(firstName: "Gildong", lastName: "Hong", age: 24, address: Optional("Jeju"))
    

    'Swift' 카테고리의 다른 글

    Swift) URLSession Network Layer  (1) 2022.08.10
    Data(contentsOf:)? URLSession?  (0) 2022.08.01
    Swift) API Design Guidelines  (0) 2021.06.10
    Swift) 함수 타입  (0) 2020.11.18
    Swift) Optional  (0) 2020.11.11

    댓글

Designed by Tistory.