Mobile Development 23 min read

Swift JSON Decoding Solutions: Comparison and Custom Implementation

The article surveys common Swift JSON decoding approaches—including manual Unbox, HandyJSON, Sourcery, built-in Codable and BetterCodable—highlights their strengths and weaknesses, then presents a custom NEJSONDecoder that adds default values, key mapping, type compatibility, tolerant error handling, and transformation support, with performance benchmarks.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
Swift JSON Decoding Solutions: Comparison and Custom Implementation

This article reviews several commonly used Swift JSON decoding solutions, compares their advantages and disadvantages, and introduces a custom decoder implementation (NEJSONDecoder) that provides default values, key mapping, type compatibility, tolerant error handling, and custom transformation.

Commonly Used Solutions

Manual Decoding (e.g., Unbox (DEPRECATED) )

Early Swift projects often used Unbox (similar to ObjectMapper). The approach requires developers to write decoding logic manually, which has a high usage cost and has been replaced by the official Codable API.

struct User {
    let name: String
    let age: Int
}

extension User: Unboxable {
    init(unboxer: Unboxer) throws {
        self.name = try unboxer.unbox(key: "name")
        self.age = try unboxer.unbox(key: "age")
    }
}

Alibaba's HandyJSON [1]

HandyJSON relies on memory rules inferred from the Swift runtime and operates directly on memory. It does not require inheritance from NSObject ; simply conform to the protocol.

class Model: HandyJSON {
    var userId: String = ""
    var nickname: String = ""
    required init() {}
}

let jsonObject: [String: Any] = [
    "userId": "1234",
    "nickname": "lilei",
]
let model = Model.deserialize(from: object)

HandyJSON has compatibility and safety issues because it tightly depends on Swift's memory layout; major Swift version upgrades may cause instability, and runtime reflection impacts performance.

Meta‑programming with Sourcery [2]

Sourcery is a Swift code generator that uses SourceKitten to parse source code and Stencil templates to generate code. It offers strong customizability.

Example: define an AutoCodable protocol and let data types conform to it.

protocol AutoCodable: Codable {}

class Model: AutoCodable {
    // sourcery: key = "userID"
    var userId: String = ""
    var nickname: String = ""
    required init(from decoder: Decoder) throws {
        try autoDecodeModel(from: decoder)
    }
}

Sourcery scans the code, finds classes/structs implementing AutoCodable , and generates the decoding implementation automatically.

Swift Built‑in API Codable

Since Swift 4.0, Codable provides a unified JSON serialization solution. The compiler automatically generates CodingKeys and init(from:) methods based on the data structure.

Issues encountered in practice include:

Key mapping is unfriendly (e.g., mapping nickname or nickName to User.name requires redefining the entire CodingKeys enum).

Lack of tolerant handling and default values; strict type checking can increase usage cost.

Nested structure parsing is cumbersome.

JSONDecoder only accepts Data , not a dictionary, which may cause performance overhead when converting types.

struct User: Codable {
    var name: String
    var age: Int
}

let json1 = "{\"name\": \"lilei\"}".data(using: .utf8)!
let json2 = "{\"nickname\": \"lilei\"}".data(using: .utf8)!
let json3 = "{\"nickName\": \"lilei\", \"city\": \"shenzhen\"}".data(using: .utf8)!

let decoder = JSONDecoder()
let user = try? decoder.decode(User.self, from: json1) // succeeds
let userFail = try? decoder.decode(User.self, from: json3) // throws because "shenzhen" cannot be decoded to City

For more details, see the open‑source JSONDecoder.swift implementation [4].

Property Wrappers such as BetterCodable [3]

Swift 5.0 introduced property wrappers, which can supplement Codable with default values and fallback strategies.

struct UserPrivilege: Codable {
    @DefaultFalse var isAdmin: Bool
}

let json = "{\"isAdmin\": null}".data(using: .utf8)!
let result = try JSONDecoder().decode(UserPrivilege.self, from: json)
print(result) // UserPrivilege(isAdmin: false)

Using wrappers requires explicit property declarations, which adds some overhead.

Comparison of Schemes

Codable

HandyJSON

BetterCodable

Sourcery

Type Compatibility

Supports Default Values

Key Mapping

Integration Cost

Safety

Performance

From the perspective of integration cost and safety, Codable is the best choice, but we still need to address its shortcomings.

Codable Deep Dive

Principle Overview

Consider the following data structures that conform to Codable :

enum Gender: Int, Codable {
    case unknown
    case male
    case female
}

struct User: Codable {
    var name: String
    var age: Int
    var gender: Gender
}

Compiling with swiftc main.swift -emit-sil | xcrun swift-demangle > main.sil reveals that the compiler automatically generates CodingKeys and the init(from: Decoder) method.

enum Gender: Int, Decodable & Encodable {
  case unknown
  case male
  case female
  init?(rawValue: Int)
  var rawValue: Int { get }
}

struct User: Decodable & Encodable {
  var name: String
  var age: Int
  var gender: Gender
  enum CodingKeys: CodingKey { case name, age, gender }
  func encode(to encoder: Encoder) throws
  init(from decoder: Decoder) throws
}

The generated init(from:) roughly performs the following steps:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: User.CodingKeys.self)
    self.name = try container.decode(String.self, forKey: .name)
    self.age = try container.decode(Int.self, forKey: .age)
    self.gender = try container.decode(Gender.self, forKey: .gender)
}

The core work happens in the Decoder and its three container protocols: KeyedDecodingContainerProtocol , UnkeyedDecodingContainerProtocol , and SingleValueDecodingContainerProtocol . The standard JSONDecoder is a concrete implementation of Decoder .

Custom Solution Design

Feature Design

Support default values.

Allow type compatibility (e.g., decode an Int JSON value into a String property).

When decoding fails, return nil instead of throwing.

Support key mapping.

Allow custom decoding logic.

Define the following protocols:

public protocol NECodableDefaultValue {
    static func codableDefaultValue() -> Self
}

extension Bool: NECodableDefaultValue { public static func codableDefaultValue() -> Self { false } }
extension Int: NECodableDefaultValue { public static func codableDefaultValue() -> Self { 0 } }
// ... other basic types

public protocol NECodableMapperValue {
    var mappingKeys: [String] { get }
}

extension String: NECodableMapperValue { public var mappingKeys: [String] { [self] } }
extension Array: NECodableMapperValue where Element == String { public var mappingKeys: [String] { self } }

public protocol NECodable: Codable {
    static var modelCustomPropertyMapper: [String: NECodableMapperValue]? { get }
    static func decodingDefaultValue
(for key: CodingKeys) -> Any?
    mutating func decodingCustomTransform(jsonObject: Any, decoder: Decoder) throws -> Bool
}

Example model using the custom protocol:

struct Model: NECodable {
    var nickName: String
    var age: Int
    static var modelCustomPropertyMapper: [String: NECodableMapperValue]? = [
        "nickName": ["nickname", "nickName"],
        "age": "userInfo.age"
    ]
    static func decodingDefaultValue
(for key: CodingKeys) -> Any? where CodingKeys: CodingKey {
        guard let key = key as? Self.CodingKeys else { return nil }
        switch key {
        case .age: return 18
        default: return nil
        }
    }
}

let jsonObject: [String: Any] = [
    "nickname": "lilei",
    "userInfo": ["age": 123]
]
let model = try NEJSONDecoder().decode(Model.self, jsonObject: jsonObject)
XCTAssert(model.nickName == "lilei")
XCTAssert(model.age == 123)

Decoder and Container Implementation

Implement NEJSONDecoder conforming to Decoder and three container types. The container’s decode methods first try normal decoding, then fall back to default values provided by the model or the NECodableDefaultValue protocol.

class NEJSONKeyedDecodingContainer
: KeyedDecodingContainerProtocol {
    public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
        do {
            return try _decode(type, forKey: key)
        } catch {
            if let value = self.defaultValue(for: key),
               let unbox = try? decoder.unbox(value, as: Bool.self) {
                return unbox
            }
            if provideDefaultValue {
                return Bool.codableDefaultValue()
            }
            throw error
        }
    }
    private func _decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
        guard let entry = self.entry(for: key) else { throw DecodingError.keyNotFound(key, .init(codingPath: [], debugDescription: "Key not found")) }
        decoder.codingPath.append(key)
        defer { decoder.codingPath.removeLast() }
        guard let value = try decoder.unbox(entry, as: Bool.self) else { throw DecodingError.typeMismatch(Bool.self, .init(codingPath: decoder.codingPath, debugDescription: "Type mismatch")) }
        return value
    }
    // ... similar implementations for other types
}

Property Wrapper Approach

Using Swift property wrappers simplifies key mapping and default value handling:

@propertyWrapper
class NECodingValue
: Codable {
    public var wrappedValue: Value
    private var keys: [String]?
    public init(wrappedValue: Value) { self.wrappedValue = wrappedValue }
    public init(wrappedValue: Value, keys: String...) { self.wrappedValue = wrappedValue; self.keys = keys }
    // Decoding initializer reads from decoder using custom keys
    required init(from decoder: Decoder) throws {
        // custom logic to fetch value using keys or fallback to default
    }
    public func encode(to encoder: Encoder) throws { try wrappedValue.encode(to: encoder) }
}

struct Model: NECodable {
    @NECodingValue(keys: "nickname") var name: String
    @NECodingValue var city: String = "hangzhou" // default if missing
    @NECodingValue var enable: Bool // default false via wrapper
}

Application Scenario Example

Define a generic network request protocol that automatically decodes JSON into NECodable models.

protocol APIRequest {
    associatedtype Model
    var path: String { get }
    var parameters: [String: Any]? { get }
    static func parse(_ data: Any) throws -> Model
}

extension APIRequest where Model: NECodable {
    static func parse(_ data: Any) throws -> Model {
        let decoder = NEJSONDecoder()
        return try decoder.decode(Model.self, jsonObject: data)
    }
}

extension APIRequest {
    @discardableResult
    func start(completion: @escaping (Result
) -> Void) -> APIToken
{
        // network request implementation using underlying HTTP client
    }
}

struct MainRequest: APIRequest {
    struct Model: NECodable {
        struct Item: NECodable { var title: String }
        var items: [Item]
        var page: Int
    }
    let path = "/api/main"
}

func doRequest() {
    MainRequest().start { result in
        switch result {
        case .success(let model): print("page index: \(model.page)")
        case .failure(let error): HUD.show(error: error)
        }
    }
}

Performance Comparison

The following chart shows the execution time of 10,000 runs for each library when converting from Data to model and from JSON object to model. JSONDecoder is fastest for Data → Model , while NEJSONDecoder excels for JSON Object → Model . HandyJSON is the slowest.

References

[1] HandyJSON: https://github.com/alibaba/HandyJSON

[2] Sourcery: https://github.com/krzysztofzablocki/Sourcery

[3] BetterCodable: https://github.com/marksands/BetterCodable

[4] JSONDecoder.swift: https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/JSONDecoder.swift

[5] test.swift: https://gist.github.com/langyanduan/4f9e773c9b25f8f829542185ea55581d#file-test-swift

This article is published by NetEase Cloud Music’s technical team. Unauthorized reproduction is prohibited. We are hiring; if you are interested, contact grp.music-fe(at)corp.netease.com.

JSONSwiftDecoderBetterCodableCodableHandyJSONSourcery
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.