@@ -162,9 +162,32 @@ npx @apidevtools/swagger-cli validate openapi.json
162162
163163---
164164
165- ## schema_type! Macro
165+ ## schema_type! Macro (RECOMMENDED)
166166
167- Generate request/response types from existing structs with field filtering. Supports cross-file references and auto-generates ` From ` impl.
167+ > ** ALWAYS prefer ` schema_type! ` over manually defining request/response structs.**
168+ >
169+ > Benefits:
170+ > - Single source of truth (your model)
171+ > - Auto-generated ` From ` impl for easy conversion
172+ > - Automatic type resolution (enums, custom types → absolute paths)
173+ > - SeaORM relation support (HasOne, BelongsTo, HasMany)
174+ > - No manual field synchronization
175+
176+ ### Why Not Manual Structs?
177+
178+ ``` rust
179+ // ❌ BAD: Manual struct definition - requires sync with Model
180+ #[derive(Serialize , Deserialize , Schema )]
181+ pub struct UserResponse {
182+ pub id : i32 ,
183+ pub name : String ,
184+ pub email : String ,
185+ // Forgot to add new field? Schema out of sync!
186+ }
187+
188+ // ✅ GOOD: Derive from Model - always in sync
189+ schema_type! (UserResponse from crate :: models :: user :: Model , omit = [" password_hash" ]);
190+ ```
168191
169192### Basic Syntax
170193
@@ -181,10 +204,43 @@ schema_type!(UpdateUserRequest from crate::models::user::Model, pick = ["name"],
181204// Rename fields
182205schema_type! (UserDTO from crate :: models :: user :: Model , rename = [(" id" , " user_id" )]);
183206
207+ // Partial updates (all fields become Option<T>)
208+ schema_type! (UserPatch from crate :: models :: user :: Model , partial );
209+
210+ // Partial updates (specific fields only)
211+ schema_type! (UserPatch from crate :: models :: user :: Model , partial = [" name" , " email" ]);
212+
213+ // Custom serde rename strategy
214+ schema_type! (UserSnakeCase from crate :: models :: user :: Model , rename_all = " snake_case" );
215+
216+ // Custom OpenAPI schema name
217+ schema_type! (Schema from Model , name = " UserSchema" );
218+
219+ // Skip Schema derive (won't appear in OpenAPI)
220+ schema_type! (InternalDTO from Model , ignore );
221+
184222// Disable Clone derive
185223schema_type! (LargeResponse from SomeType , clone = false );
186224```
187225
226+ ### Same-File Model Reference
227+
228+ When the model is in the same file, use simple name with ` name ` parameter:
229+
230+ ``` rust
231+ // In src/models/user.rs
232+ pub struct Model {
233+ pub id : i32 ,
234+ pub name : String ,
235+ pub status : UserStatus , // Custom enum - auto-resolved to absolute path
236+ }
237+
238+ pub enum UserStatus { Active , Inactive }
239+
240+ // Simple `Model` path works - module path inferred from file location
241+ vespera :: schema_type! (Schema from Model , name = " UserSchema" );
242+ ```
243+
188244### Cross-File References
189245
190246Reference structs from other files using full module paths:
@@ -231,11 +287,51 @@ Json(model.into()) // Easy conversion!
231287| ` omit ` | Exclude these fields | ` omit = ["password"] ` |
232288| ` rename ` | Rename fields | ` rename = [("id", "user_id")] ` |
233289| ` add ` | Add new fields (disables From impl) | ` add = [("extra": String)] ` |
290+ | ` partial ` | Make fields optional for PATCH | ` partial ` or ` partial = ["name"] ` |
291+ | ` name ` | Custom OpenAPI schema name | ` name = "UserSchema" ` |
292+ | ` rename_all ` | Serde rename strategy | ` rename_all = "camelCase" ` |
293+ | ` ignore ` | Skip Schema derive | bare keyword |
234294| ` clone ` | Control Clone derive (default: true) | ` clone = false ` |
235295
236- ### Use Case: Sea-ORM Models
296+ ### SeaORM Integration (RECOMMENDED)
297+
298+ ` schema_type! ` has first-class SeaORM support with automatic relation handling:
299+
300+ ``` rust
301+ // src/models/memo.rs
302+ #[derive(Clone , Debug , DeriveEntityModel )]
303+ #[sea_orm(table_name = " memo" )]
304+ pub struct Model {
305+ #[sea_orm(primary_key)]
306+ pub id : i32 ,
307+ pub title : String ,
308+ pub user_id : i32 ,
309+ pub status : MemoStatus , // Custom enum
310+ pub user : BelongsTo <super :: user :: Entity >, // → Option<Box<UserSchema>>
311+ pub comments : HasMany <super :: comment :: Entity >, // → Vec<CommentSchema>
312+ pub created_at : DateTimeWithTimeZone , // → chrono::DateTime<FixedOffset>
313+ }
314+
315+ #[derive(EnumIter , DeriveActiveEnum , Serialize , Deserialize , Schema )]
316+ pub enum MemoStatus { Draft , Published , Archived }
317+
318+ // Generates Schema with proper types - no imports needed!
319+ vespera :: schema_type! (Schema from Model , name = " MemoSchema" );
320+ ```
321+
322+ ** Automatic Type Conversions:**
237323
238- Perfect for creating API types from database models:
324+ | SeaORM Type | Generated Type | Notes |
325+ | -------------| ---------------| -------|
326+ | ` HasOne<Entity> ` | ` Box<Schema> ` or ` Option<Box<Schema>> ` | Based on FK nullability |
327+ | ` BelongsTo<Entity> ` | ` Option<Box<Schema>> ` | Always optional |
328+ | ` HasMany<Entity> ` | ` Vec<Schema> ` | |
329+ | ` DateTimeWithTimeZone ` | ` vespera::chrono::DateTime<FixedOffset> ` | No SeaORM import needed |
330+ | Custom enums | ` crate::module::EnumName ` | Auto-resolved to absolute path |
331+
332+ ** Circular Reference Handling:** Automatically detected and handled by inlining fields.
333+
334+ ### Complete Example
239335
240336``` rust
241337// src/models/user.rs (Sea-ORM entity)
@@ -246,19 +342,32 @@ pub struct Model {
246342 pub id : i32 ,
247343 pub name : String ,
248344 pub email : String ,
345+ pub status : UserStatus ,
249346 pub password_hash : String , // Never expose!
250347 pub created_at : DateTimeWithTimeZone ,
251348}
252349
253- // src/routes/users.rs
350+ // Generate Schema in same file - simple Model path
351+ vespera :: schema_type! (Schema from Model , name = " UserSchema" );
352+
353+ // src/routes/users.rs - use full path for cross-file reference
254354schema_type! (CreateUserRequest from crate :: models :: user :: Model , pick = [" name" , " email" ]);
255355schema_type! (UserResponse from crate :: models :: user :: Model , omit = [" password_hash" ]);
356+ schema_type! (UserPatch from crate :: models :: user :: Model , omit = [" password_hash" , " id" ], partial );
256357
257358#[vespera:: route(get, path = " /{id}" )]
258359pub async fn get_user (Path (id ): Path <i32 >, State (db ): State <DbPool >) -> Json <UserResponse > {
259360 let user = User :: find_by_id (id ). one (& db ). await . unwrap (). unwrap ();
260361 Json (user . into ()) // From impl handles conversion
261362}
363+
364+ #[vespera:: route(patch, path = " /{id}" )]
365+ pub async fn patch_user (
366+ Path (id ): Path <i32 >,
367+ Json (patch ): Json <UserPatch >, // All fields are Option<T>
368+ ) -> Json <UserResponse > {
369+ // Apply partial update...
370+ }
262371```
263372
264373---
0 commit comments