Note: The package was renamed from FluentContentMacro to FluentDTOMacro shortly after initial release to better reflect its purpose. If you were one of the early adopters, please update your package references and macro usage accordingly.
A Swift macro that simplifies how you handle Vapor Fluent models in your API responses. It automatically generates clean, type-safe DTOs, eliminating boilerplate while maintaining a clear separation between your database models and API layer.
- π Reduced Boilerplate - Automatic DTO generation with a simple macro
- π Type Safety - Compile-time validation of your model-to-DTO mappings
- π― Selective Fields - Control which properties are exposed in your DTOs
- π Relationship Support - Automatically handles nested Fluent relationships
- 𧡠Concurrency Ready - Generated DTOs automatically conform to Sendable
- π οΈ Customizable - Configure access levels, mutability, and protocol conformance
- π Security First - Easy exclusion of sensitive fields
- π¨ Clean Architecture - Clear separation of database and API concerns
Add the following dependency to your Package.swift
:
dependencies: [
.package(url: "https://github.com/diokaratzas/fluent-dto-macro.git", from: "1.0.0")
]
Then add the macro to your target's dependencies:
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "FluentDTOMacro", package: "fluent-dto-macro")
]
)
]
1οΈβ£ Import the package:
import FluentDTOMacro
2οΈβ£ Add the macro to your model:
@FluentDTO
final class User: Model {
@ID var id: UUID?
@Field(key: "name") var name: String
@Children(for: \.$user) var posts: [Post]
}
// The macro generates:
public struct UserDTO: CodableDTO, Equatable, Hashable, Sendable {
public let id: UUID?
public let name: String
public let posts: [PostDTO]
}
extension User {
public func toDTO() -> UserDTO {
.init(
id: id,
name: name,
posts: posts.map { $0.toDTO() }
)
}
}
3οΈβ£ Use the generated DTO:
func getUser(_ req: Request) async throws -> UserDTO {
let user = try await User.find(req.parameters.get("id"), on: req.db)
return user.toDTO() // Type-safe UserDTO struct
}
Choose which relationships to include in your DTOs:
// Only include child relationships (default)
@FluentDTO(includeRelations: .children)
final class Post: Model {
@Children(for: \.$post) var comments: [Comment]
@Parent(key: "author_id") var author: User // Will be ignored
}
// Only include parent relationships
@FluentDTO(includeRelations: .parent)
final class Comment: Model {
@Parent(key: "post_id") var post: Post
@Children(for: \.$comment) var reactions: [Reaction] // Will be ignored
}
// Include both parent and child relationships
@FluentDTO(includeRelations: .all) // Same as [.parent, .children]
final class Category: Model {
@Parent(key: "parent_id") var parent: Category
@Children(for: \.$parent) var subcategories: [Category]
}
// Include no relationships
@FluentDTO(includeRelations: .none)
final class Settings: Model {
@Parent(key: "user_id") var user: User // Will be ignored
@Children(for: \.$settings) var logs: [Log] // Will be ignored
}
// Mix and match relationships
@FluentDTO(includeRelations: [.parent, .children]) // Same as .all
final class CustomModel: Model {
@Parent(key: "parent_id") var parent: Parent
@Children(for: \.$parent) var children: [Child]
}
Control property mutability:
// Immutable properties with 'let' (default)
@FluentDTO(immutable: true)
final class Product: Model {
@ID var id: UUID?
@Field(key: "name") var name: String
@Field(key: "price") var price: Double
}
// Generated with 'let' properties:
// struct ProductDTO: CodableDTO, Equatable, Sendable {
// let id: UUID?
// let name: String
// let price: Double
// }
// Mutable properties with 'var'
@FluentDTO(immutable: false)
final class Cart: Model {
@ID var id: UUID?
@Field(key: "total") var total: Double
@Parent(key: "user_id") var user: User
}
// Generated with 'var' properties:
// struct CartDTO: CodableDTO, Equatable, Sendable {
// var id: UUID?
// var total: Double
// var user: UserDTO
// }
Set visibility levels:
// Public access (default)
@FluentDTO(accessLevel: .public)
final class Article: Model {
@ID var id: UUID?
@Field(key: "title") var title: String
}
// Generated with public access:
// public struct ArticleDTO: CodableDTO, Equatable, Sendable { ... }
// public func toDTO() -> ArticleDTO { ... }
// Internal access
@FluentDTO(accessLevel: .internal)
final class Draft: Model {
@ID var id: UUID?
@Field(key: "content") var content: String
}
// Generated with internal access:
// struct DraftDTO: CodableDTO, Equatable, Sendable { ... }
// func toDTO() -> DraftDTO { ... }
// FilePrivate access for internal caching
@FluentDTO(accessLevel: .fileprivate)
final class Cache: Model {
@ID var id: UUID?
@Field(key: "data") var data: Data
}
// Generated with fileprivate access:
// fileprivate struct CacheDTO: CodableDTO, Equatable, Sendable { ... }
// fileprivate func toDTO() -> CacheDTO { ... }
// Private access for implementation details
@FluentDTO(accessLevel: .private)
final class InternalLog: Model {
@ID var id: UUID?
@Field(key: "message") var message: String
}
// Generated with private access:
// private struct InternalLogDTO: CodableDTO, Equatable, Sendable { ... }
// private func toDTO() -> InternalLogDTO { ... }
Protect sensitive data:
final class User: Model {
@Field(key: "email") var email: String
@FluentDTOIgnore // Exclude from generated DTO
@Field(key: "password_hash") var passwordHash: String
}
Control which protocols your DTOs conform to:
// All protocols (default)
@FluentDTO(conformances: .all) // Equatable, Hashable, and Sendable
// Single protocol
@FluentDTO(conformances: .equatable) // Only Equatable
@FluentDTO(conformances: .hashable) // Only Hashable
@FluentDTO(conformances: .sendable) // Only Sendable
// Multiple protocols
@FluentDTO(conformances: [.equatable, .hashable]) // Equatable and Hashable
@FluentDTO(conformances: [.equatable, .sendable]) // Equatable and Sendable
@FluentDTO(conformances: [.hashable, .sendable]) // Hashable and Sendable
// No additional protocols
@FluentDTO(conformances: .none) // Only CodableDTO
Configure default behavior for all @FluentDTO usages in your app:
// In your app's setup code
FluentDTODefaults.immutable = false // Make all DTOs mutable by default
FluentDTODefaults.includeRelations = .all // Include all relationships by default
FluentDTODefaults.accessLevel = .internal // Use internal access by default
FluentDTODefaults.conformances = [.equatable, .hashable] // Default protocol conformances
// Individual @FluentDTO attributes still override the defaults
@FluentDTO(immutable: true) // This specific type will be immutable
- Prefer array relationships for better type safety
- Use selective relationship inclusion to prevent cycles
- Be mindful of bidirectional relationships
- Consider relationship depth when using
.both
- Leverage Many-to-Many relationships for complex associations
We welcome contributions! Whether it's:
- π Bug fixes
- β¨ New features
- π Documentation improvements
- π§ͺ Additional tests
Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
If you find FluentDTO helpful in your projects, please consider:
- Giving it a βοΈ on GitHub
- Sharing it with others
- Contributing back to the project