앱 개발에서 네트워킹을 통해 데이터를 주고받는 것은 매우 중요한 기능 중 하나입니다. 그리고 이 과정에서 가장 흔히 사용되는 데이터 형식이 JSON입니다. JSON은 간단한 텍스트 형식으로, 데이터를 인코딩(encoding) 하거나 디코딩(decoding) 하여 우리가 필요한 데이터 형태로 변환할 수 있습니다.
Swift에서는 Encodable 프로토콜을 사용해 데이터를 JSON으로 인코딩하고, Decodable 프로토콜을 통해 JSON 데이터를 원하는 데이터 형식으로 디코딩할 수 있습니다. 이 두 프로토콜을 합친 Codable 프로토콜도 있습니다. 해당 프로토콜들은 Swift에서 JSON을 쉽게 다루도록 도와줍니다.
Mock API 환경 설정
먼저 JSON을 테스트할 수 있는 Mock API 환경을 만들어야 합니다. 오픈 API를 사용해도 되지만, 다양한 응답 상황을 시뮬레이션하기 위해 Mock API 서비스를 사용하는 것을 추천합니다.
제가 추천하는 서비스 중 하나는 Insomnia인데 사용법이 매우 간단합니다.
Codable을 사용한 기본 네트워킹 예제
다음은 iOS에서 네트워킹을 할 때 기본적으로 사용할 수 있는 URLSession과 Codable을 사용해 JSON 데이터를 처리하는 간단한 예시입니다. 이처럼 Codable을 사용하면 서버로부터 받은 JSON 데이터를 쉽게 Swift 모델로 변환할 수 있습니다.
// 1️⃣ JSON 데이터
{
"name": "sookim-1",
"position": "iOS Developer"
}
// 2️⃣ Codable 모델
struct TestModel: Codable {
let name: String
let position: String
}
// 3️⃣ 네트워킹
let url = URL(string: "https://mock_c12df05abb8249f0b5d8b9a4f4fdcfe9.mock.insomnia.rest/first")!
let (data, response) = try await URLSession.shared.data(from: url)
let result = try JSONDecoder().decode(TestModel.self, from: data)
print(result)
// 4️⃣ 출력결과
// TestModel(name: "sookim-1", position: "iOS Developer")
주의사항: Codable 모델과 JSON 키 매칭
Codable 모델을 선언할 때 주의할 점들이 있습니다.
- JSON 키값과 Codable 모델의 프로퍼티 이름은 일치해야 합니다.
- JSON 값의 자료형과 Codable 모델의 프로퍼티 자료형은 일치해야 합니다.
- JSON 에 없는 키값을 Codable 모델의 프로퍼티 선언하면 디코딩에러가 발생할 수 있습니다.
Codable 모델을 선언할 때 가장 중요한 점은 JSON의 키와 Swift 모델의 프로퍼티가 정확히 일치해야 한다는 것입니다. 예를 들어, JSON에 없는 프로퍼티를 모델에 선언해두면 디코딩 에러가 발생할 수 있습니다.
// 1️⃣ JSON 데이터
{
"name": "sookim-1",
"position": "iOS Developer"
}
// 🅾️ 적합한 Codable 모델
struct TestModel: Codable {
let name: String
let position: String
}
// 🅾️ 적합한 Codable 모델: 일부 키가 없는 경우
struct TestModel: Codable {
let name: String
}
// ❎ 부적합한 Codable 모델: JSON에 없는 키 추가 시
struct TestModel: Codable {
let name: String
let position: String
let hobby: String // JSON에 이 키가 없으므로 디코딩 에러 발생
}
// ❎ 부적합한 Codable 모델: JSON에 값 자료형이 다른 경우
struct TestModel: Codable {
let name: Int // JSON에 값 자료형과 다르므로 디코딩 에러 발생
let position: String
}
Codable 모델과 JSON 키 매칭 디코딩에러 방지 방법
기본적으로 적합한 Codable모델을 작성하는 것이 명확하지만 디코딩에러를 유연하게 방지할 수 있는 방법들이 몇가지 있습니다.
- 옵셔널 자료형 사용: 만약 특정 키가 존재하지 않을 수 있다면, 해당 프로퍼티를 옵셔널 자료형으로 선언하여 에러를 방지할 수 있습니다.
struct TestModel: Codable {
let name: String
let position: String
let hobby: String? // 옵셔널 자료형
}
- 기본값 제공 : 기본값을 설정하여 에러를 방지하는 방법도 있습니다.
struct TestModel: Codable {
let name: String
let position: String
let hobby: String = "traveling"
}
응답 값이 null일 때의 처리
위에서 말한것처럼 프로퍼티 자료형이 다른 경우 디코딩에러가 발생할 수 있는데 옵셔널 자료형도 예외는 아닙니다.
서버에서 null 값을 받을 때, Swift에서는 해당 값을 nil로 처리하므로 Swift의 프로퍼티가 옵셔널이 아니라면 디코딩 에러가 발생할 수 있습니다. 이 경우,프로퍼티 자료형을 옵셔널로 설정하여 안전하게 처리할 수 있습니다.
// 1️⃣ JSON 데이터
{
"name": "fail",
"position": null
}
// 🅾️ 적합한 Codable 모델
struct TestModel: Codable {
let name: String
let position: String? // null 허용
}
CodingKey 프로토콜을 사용하여 원하는 키 값 사용하기
기본적으로 JSON 데이터의 키값과 모델의 프로퍼티 이름은 매칭되어야 하지만 모델의 프로퍼티를 원하는 이름으로 변경하고 싶은 경우 CodingKey 프로토콜을 활용할 수 있습니다.

// 1️⃣ JSON 데이터
{
"name": "sookim-1",
"position": "iOS Developer"
}
// 2️⃣ Codable 모델
struct TestModel: Codable {
let targetName: String
let targetPosition: String
enum CodingKeys: String, CodingKey {
case targetName = "name"
case targetPosition = "position"
}
}
다양한 유형의 JSON 대처 방법
Codable 모델의 프로퍼티를 옵셔널 처리하면 해결되지만 옵셔널처리를 하지 않고 매번 확인하기 어려운 경우 유연하게 대처하는 방법들이 있습니다.
1. KeyedDecodingContainer extension을 활용한 기본값 설정
JSONDecoder가 Decode를 할때는 KeyedDecodingContainer를 사용하는데 해당 객체에서 extension을 활용하면, 값이 null 또는 없는 키값이 있는 경우 기본값으로 바로 설정할 수 있습니다.
기본적인 원리는 지정한 자료형별로 디코딩을 시도하고 성공 시 해당 자료형을 반환하는 원리입니다.
아래는 KeyedDecodingContainer extension을 활용한 예시이고, (String, Bool, Double, Float, T, UInt) 자료형들을 지정했습니다.
또한 CodingUserInfoKey의 useDefaultValues 부울 값을 통해 기본값 설정 여부를 지정할 수 있습니다.
extension Decoder {
var useDefaultValues: Bool {
return (self.userInfo[.useDefaultValues] as? Bool) ?? true
}
}
extension CodingUserInfoKey {
static let useDefaultValues = CodingUserInfoKey(rawValue: "useDefaultValues")!
}
extension KeyedDecodingContainer {
// MARK: - Decoder의 userInfo 커스텀키를 통해 기본값으로 디코딩 여부 분기처리
public func decode(_ type: Bool.Type, forKey key: KeyedDecodingContainer.Key) throws -> Bool {
let decoder = try superDecoder(forKey: key)
let useDefaultValues = (decoder.userInfo[.useDefaultValues] as? Bool) ?? true
if useDefaultValues {
return try decodeWithDefault(type, forKey: key)
} else {
return try decodeStrict(type, forKey: key)
}
}
public func decode(_ type: String.Type, forKey key: KeyedDecodingContainer.Key) throws -> String {
let decoder = try superDecoder(forKey: key)
let useDefaultValues = (decoder.userInfo[.useDefaultValues] as? Bool) ?? true
if useDefaultValues {
return try decodeWithDefault(type, forKey: key)
} else {
return try decodeStrict(type, forKey: key)
}
}
public func decode(_ type: Double.Type, forKey key: KeyedDecodingContainer.Key) throws -> Double {
let decoder = try superDecoder(forKey: key)
let useDefaultValues = (decoder.userInfo[.useDefaultValues] as? Bool) ?? true
if useDefaultValues {
return try decodeWithDefault(type, forKey: key)
} else {
return try decodeStrict(type, forKey: key)
}
}
public func decode(_ type: Float.Type, forKey key: KeyedDecodingContainer.Key) throws -> Float {
let decoder = try superDecoder(forKey: key)
let useDefaultValues = (decoder.userInfo[.useDefaultValues] as? Bool) ?? true
if useDefaultValues {
return try decodeWithDefault(type, forKey: key)
} else {
return try decodeStrict(type, forKey: key)
}
}
public func decode(_ type: Int.Type, forKey key: KeyedDecodingContainer.Key) throws -> Int {
let decoder = try superDecoder(forKey: key)
let useDefaultValues = (decoder.userInfo[.useDefaultValues] as? Bool) ?? true
if useDefaultValues {
return try decodeWithDefault(type, forKey: key)
} else {
return try decodeStrict(type, forKey: key)
}
}
public func decode(_ type: UInt.Type, forKey key: KeyedDecodingContainer.Key) throws -> UInt {
let decoder = try superDecoder(forKey: key)
let useDefaultValues = (decoder.userInfo[.useDefaultValues] as? Bool) ?? true
if useDefaultValues {
return try decodeWithDefault(type, forKey: key)
} else {
return try decodeStrict(type, forKey: key)
}
}
public func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T where T: Decodable {
let decoder = try superDecoder(forKey: key)
let useDefaultValues = (decoder.userInfo[.useDefaultValues] as? Bool) ?? true
if useDefaultValues {
return try decodeWithDefault(type, forKey: key)
} else {
return try decodeStrict(type, forKey: key)
}
}
// MARK: - 기본값으로 변환하여 디코딩
private func decodeWithDefault(_ type: Bool.Type, forKey key: KeyedDecodingContainer.Key) throws -> Bool {
return try decodeIfPresent(type, forKey: key) ?? false
}
private func decodeWithDefault(_ type: String.Type, forKey key: KeyedDecodingContainer.Key) throws -> String {
return try decodeIfPresent(type, forKey: key) ?? ""
}
private func decodeWithDefault(_ type: Double.Type, forKey key: KeyedDecodingContainer.Key) throws -> Double {
return try decodeIfPresent(type, forKey: key) ?? 0.0
}
private func decodeWithDefault(_ type: Float.Type, forKey key: KeyedDecodingContainer.Key) throws -> Float {
return try decodeIfPresent(type, forKey: key) ?? 0.0
}
private func decodeWithDefault(_ type: Int.Type, forKey key: KeyedDecodingContainer.Key) throws -> Int {
return try decodeIfPresent(type, forKey: key) ?? 0
}
private func decodeWithDefault(_ type: UInt.Type, forKey key: KeyedDecodingContainer.Key) throws -> UInt {
return try decodeIfPresent(type, forKey: key) ?? 0
}
private func decodeWithDefault<T>(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T where T: Decodable {
if let value = try decodeIfPresent(type, forKey: key) {
return value
} else if let objectValue = try? JSONDecoder().decode(type, from: "{}".data(using: .utf8)!) {
return objectValue
} else if let arrayValue = try? JSONDecoder().decode(type, from: "[]".data(using: .utf8)!) {
return arrayValue
} else if let stringValue = try decode(String.self, forKey: key) as? T {
return stringValue
} else if let boolValue = try decode(Bool.self, forKey: key) as? T {
return boolValue
} else if let intValue = try decode(Int.self, forKey: key) as? T {
return intValue
} else if let uintValue = try decode(UInt.self, forKey: key) as? T {
return uintValue
} else if let doubleValue = try decode(Double.self, forKey: key) as? T {
return doubleValue
} else if let floatValue = try decode(Float.self, forKey: key) as? T {
return floatValue
}
let context = DecodingError.Context(codingPath: [key], debugDescription: "Key: <\(key.stringValue)> cannot be decoded")
throw DecodingError.dataCorrupted(context)
}
// MARK: - 기본값으로 변환하지 않고 디코딩
private func decodeStrict(_ type: Bool.Type, forKey key: KeyedDecodingContainer.Key) throws -> Bool {
guard let value = try decodeIfPresent(type, forKey: key) else {
let context = DecodingError.Context(codingPath: [key], debugDescription: "Key: <\(key.stringValue)> is null or missing")
throw DecodingError.dataCorrupted(context)
}
return value
}
private func decodeStrict(_ type: String.Type, forKey key: KeyedDecodingContainer.Key) throws -> String {
guard let value = try decodeIfPresent(type, forKey: key) else {
let context = DecodingError.Context(codingPath: [key], debugDescription: "Key: <\(key.stringValue)> is null or missing")
throw DecodingError.dataCorrupted(context)
}
return value
}
private func decodeStrict(_ type: Double.Type, forKey key: KeyedDecodingContainer.Key) throws -> Double {
guard let value = try decodeIfPresent(type, forKey: key) else {
let context = DecodingError.Context(codingPath: [key], debugDescription: "Key: <\(key.stringValue)> is null or missing")
throw DecodingError.dataCorrupted(context)
}
return value
}
private func decodeStrict(_ type: Float.Type, forKey key: KeyedDecodingContainer.Key) throws -> Float {
guard let value = try decodeIfPresent(type, forKey: key) else {
let context = DecodingError.Context(codingPath: [key], debugDescription: "Key: <\(key.stringValue)> is null or missing")
throw DecodingError.dataCorrupted(context)
}
return value
}
private func decodeStrict(_ type: Int.Type, forKey key: KeyedDecodingContainer.Key) throws -> Int {
guard let value = try decodeIfPresent(type, forKey: key) else {
let context = DecodingError.Context(codingPath: [key], debugDescription: "Key: <\(key.stringValue)> is null or missing")
throw DecodingError.dataCorrupted(context)
}
return value
}
private func decodeStrict(_ type: UInt.Type, forKey key: KeyedDecodingContainer.Key) throws -> UInt {
guard let value = try decodeIfPresent(type, forKey: key) else {
let context = DecodingError.Context(codingPath: [key], debugDescription: "Key: <\(key.stringValue)> is null or missing")
throw DecodingError.dataCorrupted(context)
}
return value
}
private func decodeStrict<T>(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T where T: Decodable {
guard let value = try decodeIfPresent(type, forKey: key) else {
let context = DecodingError.Context(codingPath: [key], debugDescription: "Key: <\(key.stringValue)> is null or missing")
throw DecodingError.dataCorrupted(context)
}
return value
}
// MARK: - 🚨 해당 부분 추가하면 키값은 포함되어있지만 자료형 다른 경우 String으로 변환해서 디코딩 되도록 하는 코드 (옵션)
public func decodeIfPresent(_ type: Bool.Type, forKey key: K) throws -> Bool? {
guard contains(key)
else { return nil }
let decoder = try superDecoder(forKey: key)
let container = try decoder.singleValueContainer()
return try? container.decode(type)
}
public func decodeIfPresent(_ type: String.Type, forKey key: K) throws -> String? {
guard contains(key)
else { return nil }
let decoder = try superDecoder(forKey: key)
let container = try decoder.singleValueContainer()
if let value = try? container.decode(type) {
return value
} else if let intValue = try? container.decode(Int.self) {
return String(intValue)
} else if let doubleValue = try? container.decode(Double.self) {
return String(doubleValue)
} else if let boolValue = try? container.decode(Bool.self) {
return String(boolValue)
}
return nil
}
public func decodeIfPresent(_ type: Double.Type, forKey key: K) throws -> Double? {
guard contains(key)
else { return nil }
let decoder = try superDecoder(forKey: key)
let container = try decoder.singleValueContainer()
if let value = try? container.decode(type) {
return value
} else if let stringValue = try? container.decode(String.self) {
return Double(stringValue)
}
return nil
}
public func decodeIfPresent(_ type: Float.Type, forKey key: K) throws -> Float? {
guard contains(key)
else { return nil }
let decoder = try superDecoder(forKey: key)
let container = try decoder.singleValueContainer()
if let value = try? container.decode(type) {
return value
} else if let stringValue = try? container.decode(String.self) {
return Float(stringValue)
}
return nil
}
public func decodeIfPresent(_ type: Int.Type, forKey key: K) throws -> Int? {
guard contains(key)
else { return nil }
let decoder = try superDecoder(forKey: key)
let container = try decoder.singleValueContainer()
if let value = try? container.decode(type) {
return value
} else if let stringValue = try? container.decode(String.self) {
return Int(stringValue)
}
return nil
}
public func decodeIfPresent(_ type: UInt.Type, forKey key: K) throws -> UInt? {
guard contains(key)
else { return nil }
let decoder = try superDecoder(forKey: key)
let container = try decoder.singleValueContainer()
if let value = try? container.decode(type) {
return value
} else if let stringValue = try? container.decode(String.self) {
return UInt(stringValue)
}
return nil
}
public func decodeIfPresent<T>(_ type: T.Type, forKey key: K) throws -> T? where T: Decodable {
guard contains(key)
else { return nil }
let decoder = try superDecoder(forKey: key)
let container = try decoder.singleValueContainer()
return try? container.decode(type)
}
}
https://gist.github.com/sookim-1/24118584ae49a5c1f5d11e03a4c50de9
2. 응답 값의 자료형이 변경될 수 있는 경우
API의 응답 값이 파라미터에 따라 항상 같은 자료형으로 오지 않는 경우도 있습니다. 예를 들어, 동일한 키가 때로는 문자열로, 때로는 객체로 전달될 수 있습니다. 이런 상황을 처리하기 위해 여러 자료형에 대응하는 방법을 사용할 수 있습니다.
예시로 JSON의 hobby키 값의 자료형이 변경된다고 가정해보겠습니다.
// 1️⃣ hobby가 String인 경우
{
"name": "sookim-1",
"position": "iOS Developer"
"hobby": "traveling"
}
// 2️⃣ hobby가 객체인 경우
{
"name": "sookim-1",
"position": "iOS Developer"
"hobby": {
"first": "traveling",
"second": "gaming"
}
}
해결 방법으로는 hobby가 다양한 일 수 있으므로, 이를 처리할 수 있는 방법은 자료형별로 디코딩을 시도해보는 것 입니다.
enum GenericTypeJSONKey<T: Codable>: Codable {
case element(T)
case string(String)
case int(Int)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let element = try? container.decode(T.self) {
self = .element(element)
} else if let string = try? container.decode(String.self) {
self = .string(string)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else {
throw DecodingError.typeMismatch(GenericTypeJSONKey.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unknown type"))
}
}
}
struct TestModel: Codable {
var name: String
var position: String
var hobby: GenericMessage<HobbyModel>?
}
struct HobbyModel: Codable {
var first: String
var second: String
}
// API 호출 메서드
func callTestAPI(complete: @escaping(Result<(TestModel), Error>) -> Void)
// 응답 후 사용 예시
self.callTestAPI { result in
switch result {
case .success(let success):
if let hobbyType = success.hobby {
switch hobbyType {
case .element(let realValue):
print("Generic 응답 결과 : \(realValue)")
case .string(let mockValue):
print("String 응답 결과 : \(mockValue)")
case .int(let mockValue):
print("Int 응답 결과 : \(mockValue)")
}
}
case .failure(let failure):
print(failure.errorDescription)
}
}
이렇게 하면 여러 가지 자료형의 응답을 안전하게 처리할 수 있습니다.
3. JSONString
JSONString이란, JSON 또는 Dictionary 형태를 띄고있는 문자열입니다.
때로는 JSON 데이터가 문자열로 감싸진 경우가 있는데, 이를 처리하는 방법도 알아둬야 합니다.
예시 — 해당 응답값 중 hobby 키 값 (string이면서 JSON 또는 dictionary형태)
SwiftyJSON 라이브러리로 파싱하는 방법
JSONString을 String 자료형으로 변경 → Dictionary 자료형으로 변경 → JSON 으로 변경
let bodyStrig = dataDict["message"].stringValue
let realBody = JSON(bodyStrig.getJsonDataDict())
문자열을 Dictionary로 변환한 후 파싱하는 방법
extension String {
func convertToDictionary() -> [String: Any]? {
guard let data = self.data(using: .utf8) else { return nil }
return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
}
}
let jsonString = "{\\"first\\": \\"traveling\\", \\"second\\": "gaming"}"
if let dict = jsonString.convertToDictionary() {
print(dict)
}
4. JSON 파싱
네트워킹이 아닌 직접 JSON을 빠르게 테스트하고 싶은 경우 테스트 할 수 도있습니다.
func decodeJSONString<T: Codable>(jsonString: String, modelType: T.Type) -> T? {
if let jsonData = jsonString.data(using: .utf8) {
let decoder = JSONDecoder()
do {
let decodedModel = try decoder.decode(modelType, from: jsonData)
print("Successd: \(decodedModel)")
return decodedModel
} catch {
print("Failed: \(error)")
}
}
return nil
}
let jsonString = """
{
"name": "sookim-1",
"position": "iOS Developer"
}
"""
struct TestModel: Codable {
var name: String
var position: String
}
if let decodedObject = decodeJSONString(jsonString: jsonString, modelType: TestModel.self) {
print("Successd: \(decodedObject)")
} else {
print("Failed")
}
결론
JSON을 다루는 작업은 iOS 개발에서 매우 빈번하게 발생하는 작업입니다. Codable을 활용하면 JSON 데이터를 손쉽게 다룰 수 있지만, 디코딩 시 자료형이나 키를 정확히 맞추지 않으면 에러가 발생할 수 있습니다. 다양한 대응 방법들을 알아두고 적재적소에 사용한다면 편리하게 처리를 할 수 있습니다.