Skip to content

schibsted/codable-macro

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

codable-macro

A Swift macro that can generate Codable implementations.

Motivation

Using Codable is the standard approach to serialization in Swift (for a number of reasons). In simple cases, using it is as simple as conforming the type to the Codable and letting the compiler synthesize all the boilerplate.

In real-world projects, however, things are rarely that simple. The JSON data that needs to be deserialized often has a different structure, different key names, invalid values, etc. Codable tries to accommodate these issues (for example, by supporting custom decoding strategies for keys), but often this is not enough which means having to write massive amounts of boilerplate code by hand.

Another feature of Codable that may cause issues is error handling. By default, decoding errors are propagated all the way up, which means a single type deep down the object tree failing to decode causes the entire object tree to fail to decode. Generally, this is a reasonable error handling strategy which is consistent with the Swift philosophy of failing early, but it is not always optimal. Sometimes potential incompleteness of the decoded data is acceptable and even preferrable over breaking features for hundreds of thousands of users, but the only way to make the decoding logic more robust is to implement it by hand.

Goal

The goal of this project is to provide an intuitive, easy to use way to generate robust serialization logic.

Features

Conform a type to Codable

To make a type codable, apply the @Codable macro to it:

@Codable
public struct Foo {
    let bar: String
}
Macro expansion
@Codable
struct Foo {
    let bar: String
    
    init(
        bar: String
    ) {
        self.bar = bar
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        bar = try container.decode(String.self, forKey: .bar)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(bar, forKey: .bar)
    }

    enum CodingKeys: String, CodingKey {
        case bar
    }
}

extension Foo: Codable {
}

Examples

JSON Decoded value
{ "bar": "hello world" } Foo(bar: "hello world")

NOTE: If you only need Decodable or Encodable conformance, you can use the @Decodable or @Encodable macros instead.

Optional properties

@Codable
public struct Foo {
    let bar: String?
}
Macro expansion
@Codable
struct Foo {
    let bar: String
    
    init(
        bar: String? = nil
    ) {
        self.bar = bar
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        do {
            bar = try container.decode(String.self, forKey: .bar)
        } catch {
            bar = nil
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encodeIfPresent(bar, forKey: .bar)
    }

    enum CodingKeys: String, CodingKey {
        case bar
    }
}

extension Foo: Codable {
}

NOTE: If an optional property fails to decode for some reason, the generated decoding logic will fall back to nil instead of throwing the error. Also, nil will be the default value of the corresponding parameter of the generated memberwise initializer.

Examples

JSON Decoded value
{ "bar": "hello world" } Foo(bar: "hello world")
{ "bar": null } Foo(bar: nil)
{ "bar": 0 } Foo(bar: nil)

Default values

If you would like to specify a default value to use during decoding, you can do it just like you normally would for non-codable types:

@Codable
public struct Foo {
    var bar: String = "some default value"
}
Macro expansion
@Codable
struct Foo {
    let bar: String
    
    init(
        bar: String = "some default value"
    ) {
        self.bar = bar
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        do {
            bar = try container.decode(String.self, forKey: .bar)
        } catch {
            bar = "some default value"
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(bar, forKey: .bar)
    }

    enum CodingKeys: String, CodingKey {
        case bar
    }
}

extension Foo: Codable {
}

NOTE: Similarly to the way optional properties are handled, if a property with a default value fails to decode for some reason, the generated decoding logic will fall back to the default value instead of throwing the error. Also, the default value will be used in the generated memberwise initializer.

Examples

JSON Decoded value
{ "bar": "hello world" } Foo(bar: "hello world")
{ "bar": null } Foo(bar: "some default value")
{ "bar": 0 } Foo(bar: "some default value")

Lossy decoding of collection types

If an item inside a JSON array fails to decode, it is quietly discarded. This applies to dictionary values as well.

@Codable
public struct Foo {
    let bar: [String]
}
Macro expansion
@Codable
public struct Foo {
    let bar: [String]
    
    init(
        bar: Array<String>
    ) {
        self.bar = bar
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        bar = try container.decode([FailableContainer<String>].self, forKey: .bar).compactMap {
            $0.wrappedValue
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(bar, forKey: .bar)
    }

    enum CodingKeys: String, CodingKey {
        case bar
    }

    private struct FailableContainer<T>: Decodable where T: Decodable {
        var wrappedValue: T?

        init(from decoder: Decoder) throws {
            wrappedValue = try? decoder.singleValueContainer().decode(T.self)
        }
    }        
}

Examples

JSON Decoded value
{ "bar": ["hello world"] } Foo(bar: ["hello world"])
{ "bar": ["I'm a string", 42] } Foo(bar: ["I'm a string"])

Custom coding keys

If the name of a property doesn't match the JSON, you can specify the JSON name using the @CodableKey("name") macro. If you need to decode a property from a nested object, you can specify the key path to the data using the familiar dot-separated key syntax: @CodableKey("path.to.name").

@Codable
struct Foo {
    @CodableKey("__baz")
    var baz: Int

    @CodableKey("qux.bar")
    var bar: String
}
Macro expansion
struct Foo {
    @CodableKey("__baz")
    var baz: Int

    @CodableKey("qux.bar")
    var bar: String
    
    init(
        baz: Int,
        bar: String
    ) {
        self.baz = baz
        self.bar = bar
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        baz = try container.decode(Int.self, forKey: .baz)

        do {
            let quxContainer = try container.nestedContainer(keyedBy: CodingKeys.QuxCodingKeys.self, forKey: .qux)
            bar = try quxContainer.decode(String.self, forKey: .bar)
        } catch {
            throw error
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        var quxContainer = container.nestedContainer(keyedBy: CodingKeys.QuxCodingKeys.self, forKey: .qux)

        try container.encode(baz, forKey: .baz)
        try quxContainer.encode(bar, forKey: .bar)
    }

    enum CodingKeys: String, CodingKey {
        case baz = "__baz", qux

        enum QuxCodingKeys: String, CodingKey {
            case bar
        }
    }
}

extension Foo: Codable {
}

Examples

JSON Decoded value
{ "__baz": 11, "qux": { "bar": "a deeply nested string" } } Foo(baz: 11, bar: "a deeply nested string")

Ignore certain properties

If you need to ignore certain properties, apply the @CodableIgnored macro to them.

@Codable
struct Foo {
    @CodableIgnored
    var uuid: UUID = UUID()
    var bar: String
}
Macro expansion
@Codable
struct Foo {
    @CodableIgnored
    var uuid: UUID = UUID()
    var bar: String
    
    init(
        uuid: UUID = UUID(),
        bar: String
    ) {
        self.uuid = uuid
        self.bar = bar
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        bar = try container.decode(String.self, forKey: .bar)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(bar, forKey: .bar)
    }

    enum CodingKeys: String, CodingKey {
        case bar
    }
}

extension Foo: Codable {
}

Examples

JSON Decoded value
{ "bar": "hello world" } Foo(uuid: 57FCCD12-7DE6-4BE9-9F16-A5B164A47D8F, bar: "hello world")

Specify custom decoding logic for certain properties

If simply decoding a property is not enough and you need to transform it in some way, mark it with the @CustomDecoded macro and provide the custom decoding logic in a static throwing function named decodeXXX:

@Codable
struct Foo {
    var qux: Int

    @CustomDecoded
    var bar: String

    static func decodeBar(from decoder: Decoder) throws -> String {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let value = try container.decode(String.self, forKey: .bar)
        return "Fancy custom decoded \(value)!"
    }
}
Macro expansion
@Codable
struct Foo {
    var qux: Int

    @CustomDecoded
    var bar: String

    static func decodeBar(from decoder: Decoder) throws -> String {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let value = try container.decode(String.self, forKey: .bar)
        return "Fancy custom decoded \(value)!"
    }
    
    init(
        qux: Int,
        bar: String
    ) {
        self.qux = qux
        self.bar = bar
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        qux = try container.decode(Int.self, forKey: .qux)
        bar = try Self.decodeBar(from: decoder)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(qux, forKey: .qux)
        try container.encode(bar, forKey: .bar)
    }

    enum CodingKeys: String, CodingKey {
        case bar, qux
    }
}

extension Foo: Codable {
}

Examples

JSON Decoded value
{ "qux": 42, "bar": "hello world" } Foo(qux: 42, bar: "Fancy custom decoded hello world!")

Custom validation logic

If you need to provide additional validation logic for your codable types, use the needsValidation parameter: @Codable(needsValidation: true) (or @Codable(needsValidation: true)) and place your validation logic in the computed property named isValid:

@Codable(needsValidation: true)
struct Foo {
    var qux: Int

    var isValid: Bool {
        qux <= 9000
    }
}
Macro expansion
@Codable(needsValidation: true)
struct Foo {
    var qux: Int

    var isValid: Bool {
        qux <= 9000
    }
    
    init(
        qux: Int
    ) {
        self.qux = qux
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        qux = try container.decode(Int.self, forKey: .qux)

        if !self.isValid {
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Validation failed"))
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(qux, forKey: .qux)
    }

    enum CodingKeys: String, CodingKey {
        case qux
    }        
}

extension Foo: Codable {
}

Examples

JSON Decoded value
{ "qux": 42 } Foo(qux: 42)
{ "qux": 9001 } DecodingError.dataCorrupted(debugDescription: "Validation failed")

Generate a memberwise initializer

Applying @Codable or @Decodable macros to a type generates a memberwise initializer as well, with the same access level as the type. You can also generate a memberwise initializer by applying the @MemberwiseInitializable macro to the type:

@MemberwiseInitializable
public struct Foo {
    let bar: String
}
Macro expansion
@MemberwiseInitializable
public struct Foo {
    let bar: String

    public init(
        bar: String
    ) {
        self.bar = bar
    }
}

You can also specify the desired access level:

@MemberwiseInitializable(.fileprivate)
public struct Foo {
    let bar: String
}
Macro expansion
@MemberwiseInitializable(.fileprivate)
public struct Foo {
    let bar: String

    fileprivate init(
        bar: String
    ) {
        self.bar = bar
    }
}

See main.swift for more examples.

NOTICE

Copyright 2025 Schibsted News Media AB.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

See the License for the specific language governing permissions and limitations under the License.

About

A Swift macro that can generate Codable implementations

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •  

Languages