A Swift macro that can generate Codable implementations.
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.
The goal of this project is to provide an intuitive, easy to use way to generate robust serialization logic.
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 {
}
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.
@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.
JSON | Decoded value |
---|---|
{ "bar": "hello world" } |
Foo(bar: "hello world") |
{ "bar": null } |
Foo(bar: nil) |
{ "bar": 0 } |
Foo(bar: nil) |
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.
JSON | Decoded value |
---|---|
{ "bar": "hello world" } |
Foo(bar: "hello world") |
{ "bar": null } |
Foo(bar: "some default value") |
{ "bar": 0 } |
Foo(bar: "some default value") |
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)
}
}
}
JSON | Decoded value |
---|---|
{ "bar": ["hello world"] } |
Foo(bar: ["hello world"]) |
{ "bar": ["I'm a string", 42] } |
Foo(bar: ["I'm a string"]) |
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 {
}
JSON | Decoded value |
---|---|
{ "__baz": 11, "qux": { "bar": "a deeply nested string" } } |
Foo(baz: 11, bar: "a deeply nested string") |
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 {
}
JSON | Decoded value |
---|---|
{ "bar": "hello world" } |
Foo(uuid: 57FCCD12-7DE6-4BE9-9F16-A5B164A47D8F, bar: "hello world") |
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 {
}
JSON | Decoded value |
---|---|
{ "qux": 42, "bar": "hello world" } |
Foo(qux: 42, bar: "Fancy custom decoded hello world!") |
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 {
}
JSON | Decoded value |
---|---|
{ "qux": 42 } |
Foo(qux: 42) |
{ "qux": 9001 } |
DecodingError.dataCorrupted(debugDescription: "Validation failed") |
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.
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.