Ana Sayfa / Blog / Swift Macro yazdım: annotation boilerplate’i nasıl yok ettim

Swift Macro yazdım: annotation boilerplate’i nasıl yok ettim

Swift Macro’ları 2024’te tüketici olarak kullanıyordum (@Observable, #Preview). 2025’te kendi macro’umu yazmam gerekti. Boilerplate kaldırmak için yazılmış bir tool’un öğrenme eğrisi de öyle. Başardığım ve başaramadığım yerleri paylaşayım. Problem: Codable key mapping boilerplate API response’ları snake_case, modellerim camelCase. Her struct için CodingKeys enum’u elle yazmak 50 modelden sonra yorucu olmaya başladı. JSONDecoder‘ın keyDecodingStrategy‘si genelde yeter […]

Swift Macro’ları 2024’te tüketici olarak kullanıyordum (@Observable, #Preview). 2025’te kendi macro’umu yazmam gerekti. Boilerplate kaldırmak için yazılmış bir tool’un öğrenme eğrisi de öyle. Başardığım ve başaramadığım yerleri paylaşayım.

Problem: Codable key mapping boilerplate

API response’ları snake_case, modellerim camelCase. Her struct için CodingKeys enum’u elle yazmak 50 modelden sonra yorucu olmaya başladı. JSONDecoder‘ın keyDecodingStrategy‘si genelde yeter ama bazı field’larda custom isim gerekiyordu.

Elimde şu vardı:

struct User: Codable {
    let id: Int
    let userName: String
    let createdAt: Date
    enum CodingKeys: String, CodingKey {
        case id
        case userName = "user_name"
        case createdAt = "created_at"
    }
}

Hedefim şuydu:

@SnakeCodable
struct User: Codable {
    let id: Int
    let userName: String
    let createdAt: Date
}

Macro package kurulumu

Terminal:

swift package init --type macro --name SnakeCodable

Xcode template’i da var ama SPM daha temiz bir başlangıç veriyor. Package içinde iki target oluyor: SnakeCodableMacros (macro implementation) ve SnakeCodable (public API).

Implementation tarafı

Member macro olarak tanımladım; struct’a yeni member (CodingKeys enum) ekleyecek:

public struct SnakeCodableMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let structDecl = declaration.as(StructDeclSyntax.self) else {
            throw MacroError.notAStruct
        }
        let properties = structDecl.memberBlock.members
            .compactMap { $0.decl.as(VariableDeclSyntax.self) }
            .flatMap { $0.bindings.compactMap { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text } }
        let cases = properties.map { prop in
            let snake = camelToSnake(prop)
            return prop == snake ? "case (prop)" : "case (prop) = "(snake)""
        }.joined(separator: "n    ")
        return ["""
        enum CodingKeys: String, CodingKey {
            (raw: cases)
        }
        """]
    }
}

camelToSnake helper

private func camelToSnake(_ input: String) -> String {
    var result = ""
    for ch in input {
        if ch.isUppercase {
            if !result.isEmpty { result += "_" }
            result += ch.lowercased()
        } else {
            result.append(ch)
        }
    }
    return result
}

Public API

@attached(member, names: named(CodingKeys))
public macro SnakeCodable() = #externalMacro(
    module: "SnakeCodableMacros",
    type: "SnakeCodableMacro"
)

names parametresi önemli; eklediğin member isimlerini compiler’a bildirmezsen macro reddediliyor.

İlk güçlük: nested type’lar

İlk versiyonumda let address: Address gibi nested type’ları doğru işliyordum ama let tags: [Tag] dizilerinde patladı. Çünkü sadece identifier pattern değil aynı zamanda type annotation’a bakmam gerekiyordu; normalde CodingKeys için sadece property name lazım, type önemli değil. Bu yanılgım için yarım gün kaybettim. Ders: macro debug etmek zor, minimal test case’le başlamak şart.

İkinci güçlük: macro’nun görünmeyen output’u

Xcode’un “Expand Macro” özelliği yokken macro’nun ne ürettiğini görmek için -dump-macro-expansions flag’ini denedim. Sonunda Xcode 15.4’ten sonra ekrana sağ tıklayıp Expand Macro direkt çalıştı. Debug sürecinde bu özelliği bilmeden vakit harcıyorsun.

Test stratejisi

SwiftSyntaxMacrosTestSupport paketi assertion helper’ları sunuyor:

import SwiftSyntaxMacrosTestSupport
import XCTest
final class SnakeCodableTests: XCTestCase {
    func testBasicStruct() {
        assertMacroExpansion("""
        @SnakeCodable
        struct User {
            let userName: String
        }
        """, expandedSource: """
        struct User {
            let userName: String
            enum CodingKeys: String, CodingKey {
                case userName = "user_name"
            }
        }
        """, macros: ["SnakeCodable": SnakeCodableMacro.self])
    }
}

Ne zaman macro yazmamalı

Tek sefer kullanacağın bir boilerplate için macro yazma; sade function yaz. Macro yazmak 2-3 saatlik bir iş ve test etmesi zor. Boilerplate 10’dan fazla yerde aynıysa, muhtemelen 10 kere daha yazacaksın; o zaman değer.

Sonuç

Macro yazmak @Observable kullanmak kadar şeker değil. Ama doğru yerde kullanırsan 300 satır boilerplate 30 satıra iniyor. Tavsiyem: ilk macro’nu küçük bir scope’ta yaz, hata mesajlarını iyi olması için emek ver, karmaşık case’lere atlama. Macro’nun mesajı kötüyse sen de kullanmayı bırakırsın.

Bu konuda bir projeniz mi var?

Kısa bir özet bırakın, 24 saat içinde size dönüş yapayım.

İletişime Geç