Skip to content

Commit 586c8dc

Browse files
committed
Implement omit_default
1 parent 614fc9c commit 586c8dc

10 files changed

Lines changed: 168 additions & 43 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"Cargo.toml":"Patch"},"note":"Add omit_default to schema_type!","date":"2026-02-18T16:50:22.753492900Z"}

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,52 @@ schema_type!(UserDTO from User, rename_all = "camelCase");
363363
// Available: "camelCase", "snake_case", "PascalCase", "SCREAMING_SNAKE_CASE", etc.
364364
```
365365

366+
### Omit Fields with Database Defaults (`omit_default`)
367+
368+
Automatically omit fields that have database-level defaults — perfect for create DTOs where the database handles `id`, `created_at`, etc.:
369+
370+
```rust
371+
#[derive(DeriveEntityModel)]
372+
#[sea_orm(table_name = "posts")]
373+
pub struct Model {
374+
#[sea_orm(primary_key)] // ← has default (auto-increment)
375+
pub id: i32,
376+
pub title: String,
377+
pub content: String,
378+
#[sea_orm(default_value = "NOW()")] // ← has default (SQL function)
379+
pub created_at: DateTimeWithTimeZone,
380+
}
381+
382+
// Omits `id` (primary_key) and `created_at` (default_value) automatically
383+
schema_type!(CreatePostRequest from crate::models::post::Model, omit_default);
384+
// Generated struct only has: title, content
385+
```
386+
387+
`omit_default` detects fields with:
388+
- `#[sea_orm(primary_key)]` — auto-increment / generated IDs
389+
- `#[sea_orm(default_value = "...")]` — SQL defaults like `NOW()`, `gen_random_uuid()`, literals
390+
391+
Can be combined with other parameters:
392+
393+
```rust
394+
// omit_default + add extra fields
395+
schema_type!(CreateItemRequest from Model, omit_default, add = [("tags": Vec<String>)]);
396+
```
397+
398+
### Database Defaults in OpenAPI
399+
400+
Fields with database defaults automatically get `default` values in the generated OpenAPI schema:
401+
402+
| SeaORM Attribute | OpenAPI Default |
403+
|-----------------|-----------------|
404+
| `primary_key` (Uuid) | `"00000000-0000-0000-0000-000000000000"` |
405+
| `primary_key` (i32/i64) | `0` |
406+
| `default_value = "NOW()"` | `"1970-01-01T00:00:00+00:00"` |
407+
| `default_value = "gen_random_uuid()"` | `"00000000-0000-0000-0000-000000000000"` |
408+
| `default_value = "true"` | `true` (literal passthrough) |
409+
410+
> **Note:** `required` is determined solely by nullability (`Option<T>`). Fields with defaults are still `required` unless they are `Option<T>`.
411+
366412
### SeaORM Integration
367413

368414
`schema_type!` has first-class support for SeaORM models with relations:
@@ -434,6 +480,7 @@ When `multipart` is enabled:
434480
| `rename_all` | Serde rename strategy: `rename_all = "camelCase"` |
435481
| `ignore` | Skip Schema derive (bare keyword, no value) |
436482
| `multipart` | Derive `TryFromMultipart` instead of serde (bare keyword) |
483+
| `omit_default` | Auto-omit fields with DB defaults: `primary_key`, `default_value` (bare keyword) |
437484

438485
---
439486

@@ -547,6 +594,12 @@ This automatically:
547594
| `Vec<T>` | `array` with items |
548595
| `Option<T>` | nullable T |
549596
| `HashMap<K, V>` | `object` with additionalProperties |
597+
| `BTreeSet<T>`, `HashSet<T>` | `array` with `uniqueItems: true` |
598+
| `Uuid` | `string` with `format: uuid` |
599+
| `Decimal` | `string` with `format: decimal` |
600+
| `NaiveDate` | `string` with `format: date` |
601+
| `NaiveTime` | `string` with `format: time` |
602+
| `DateTime`, `DateTimeWithTimeZone` | `string` with `format: date-time` |
550603
| `FieldData<NamedTempFile>` | `string` with `format: binary` |
551604
| Custom struct | `$ref` to components/schemas |
552605

SKILL.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,14 @@ pub struct User { id: u32, name: String }
3939
| `f32`, `f64` | `number` | |
4040
| `bool` | `boolean` | |
4141
| `Vec<T>` | `array` + items | |
42+
| `BTreeSet<T>`, `HashSet<T>` | `array` + items + `uniqueItems: true` | Set types |
4243
| `Option<T>` | T (nullable context) | Parent marks as optional |
4344
| `HashMap<K,V>` | `object` + additionalProperties | |
45+
| `Uuid` | `string` + `format: uuid` | |
46+
| `Decimal` | `string` + `format: decimal` | |
47+
| `NaiveDate` | `string` + `format: date` | |
48+
| `NaiveTime` | `string` + `format: time` | |
49+
| `DateTime`, `DateTimeWithTimeZone` | `string` + `format: date-time` | |
4450
| `FieldData<NamedTempFile>` | `string` + `format: binary` | File upload field |
4551
| `()` | empty response | 204 No Content |
4652
| Custom struct | `$ref` | Must derive Schema |
@@ -112,7 +118,7 @@ pub struct UserResponse {
112118
#[serde(rename = "fullName")] // ✅ Respected
113119
name: String, // → "fullName" in JSON Schema
114120

115-
#[serde(default)] //Marks as optional in schema
121+
#[serde(default)] //Recognized (does NOT affect `required` — only Option<T> does)
116122
bio: Option<String>,
117123

118124
#[serde(skip)] // ✅ Excluded from schema
@@ -188,6 +194,7 @@ npx @apidevtools/swagger-cli validate openapi.json
188194
**Primary Parameters (USE THESE):**
189195
- `pick = [...]` - Allowlist: include ONLY these fields
190196
- `omit = [...]` - Denylist: exclude these fields
197+
- `omit_default` - Auto-omit fields with DB defaults (primary_key, default_value)
191198

192199
**Advanced Parameters (USE SPARINGLY):**
193200
- `partial` - For PATCH endpoints only
@@ -232,6 +239,12 @@ schema_type!(UserPatch from crate::models::user::Model, partial);
232239
// Partial updates (specific fields only)
233240
schema_type!(UserPatch from crate::models::user::Model, partial = ["name", "email"]);
234241

242+
// Auto-omit fields with DB defaults (primary_key, default_value = "...")
243+
schema_type!(CreatePostRequest from crate::models::post::Model, omit_default);
244+
245+
// Combine omit_default with add
246+
schema_type!(CreateItemRequest from crate::models::item::Model, omit_default, add = [("tags": Vec<String>)]);
247+
235248
// Custom serde rename strategy
236249
schema_type!(UserSnakeCase from crate::models::user::Model, rename_all = "snake_case");
237250

@@ -322,6 +335,7 @@ Json(model.into()) // Easy conversion!
322335
|-----------|-------------|---------|
323336
| `pick` | Include only these fields | `pick = ["name", "email"]` |
324337
| `omit` | Exclude these fields | `omit = ["password"]` |
338+
| `omit_default` | Auto-omit fields with DB defaults | `omit_default` (bare keyword) |
325339

326340
**Situational (Use When Needed):**
327341

@@ -379,6 +393,10 @@ vespera::schema_type!(Schema from Model, name = "MemoSchema");
379393

380394
**Circular Reference Handling:** Automatically detected and handled by inlining fields.
381395

396+
**Database Defaults in OpenAPI:** Fields with `#[sea_orm(default_value = "...")]` or `#[sea_orm(primary_key)]` automatically get `default` values in the generated OpenAPI schema. SQL functions like `NOW()` and `gen_random_uuid()` are mapped to type-appropriate defaults.
397+
398+
**Required Logic:** `required` is determined **solely by nullability** (`Option<T>`). Fields with `#[serde(default)]` or `#[serde(skip_serializing_if)]` are still `required` unless they are `Option<T>`.
399+
382400
### Complete Example
383401

384402
```rust
@@ -477,6 +495,7 @@ tempfile = "3" # For NamedTempFile file uploads
477495
```rust
478496
// ✅ RECOMMENDED PATTERNS
479497
schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]);
498+
schema_type!(CreatePostRequest from crate::models::post::Model, omit_default);
480499
schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]);
481500
schema_type!(UserListItem from crate::models::user::Model, pick = ["id", "name"]);
482501

crates/vespera_macro/src/schema_macro/input.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
//! Defines input structures for `schema!` and `schema_type!` macros.
44
55
use syn::{
6-
Ident, LitStr, Token, Type, bracketed, parenthesized,
6+
bracketed, parenthesized,
77
parse::{Parse, ParseStream},
88
punctuated::Punctuated,
9+
Ident, LitStr, Token, Type,
910
};
1011

1112
/// Input for the schema! macro
@@ -90,6 +91,7 @@ impl Parse for SchemaInput {
9091
/// Or: `schema_type!(NewTypeName from SourceType, ignore)` - skip Schema derive
9192
/// Or: `schema_type!(NewTypeName from SourceType, name = "CustomName")` - custom `OpenAPI` name
9293
/// Or: `schema_type!(NewTypeName from SourceType, rename_all = "camelCase")` - serde `rename_all`
94+
#[allow(clippy::struct_excessive_bools)]
9395
pub struct SchemaTypeInput {
9496
/// The new type name to generate
9597
pub new_type: Ident,
@@ -123,6 +125,9 @@ pub struct SchemaTypeInput {
123125
/// Whether to generate a multipart/form-data struct (derives `TryFromMultipart` instead of serde)
124126
/// Use `multipart` bare keyword to set this to true.
125127
pub multipart: bool,
128+
/// Whether to omit fields that have database defaults (sea_orm `default_value` or `primary_key`).
129+
/// Use `omit_default` bare keyword to set this to true.
130+
pub omit_default: bool,
126131
}
127132

128133
/// Mode for the `partial` keyword in `schema_type`!
@@ -204,6 +209,7 @@ impl Parse for SchemaTypeInput {
204209
let mut schema_name = None;
205210
let mut rename_all = None;
206211
let mut multipart = false;
212+
let mut omit_default = false;
207213

208214
// Parse optional parameters
209215
while input.peek(Token![,]) {
@@ -293,11 +299,15 @@ impl Parse for SchemaTypeInput {
293299
// bare `multipart` - derive TryFromMultipart instead of serde
294300
multipart = true;
295301
}
302+
"omit_default" => {
303+
// bare `omit_default` - omit fields with database defaults
304+
omit_default = true;
305+
}
296306
_ => {
297307
return Err(syn::Error::new(
298308
ident.span(),
299309
format!(
300-
"unknown parameter: `{ident_str}`. Expected `omit`, `pick`, `rename`, `add`, `clone`, `partial`, `ignore`, `name`, `rename_all`, or `multipart`"
310+
"unknown parameter: `{ident_str}`. Expected `omit`, `pick`, `rename`, `add`, `clone`, `partial`, `ignore`, `name`, `rename_all`, `multipart`, or `omit_default`"
301311
),
302312
));
303313
}
@@ -325,6 +335,7 @@ impl Parse for SchemaTypeInput {
325335
schema_name,
326336
rename_all,
327337
multipart,
338+
omit_default,
328339
})
329340
}
330341
}
@@ -699,4 +710,34 @@ mod tests {
699710
assert!(input.multipart);
700711
assert!(matches!(input.partial, Some(PartialMode::All)));
701712
}
713+
714+
#[test]
715+
fn test_parse_schema_type_input_with_omit_default() {
716+
let tokens = quote::quote!(CreateUser from Model, omit_default);
717+
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
718+
assert!(input.omit_default);
719+
}
720+
721+
#[test]
722+
fn test_parse_schema_type_input_with_omit_default_and_omit() {
723+
let tokens = quote::quote!(CreateUser from Model, omit_default, omit = ["password"]);
724+
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
725+
assert!(input.omit_default);
726+
assert_eq!(input.omit.unwrap(), vec!["password"]);
727+
}
728+
729+
#[test]
730+
fn test_parse_schema_type_input_with_omit_default_and_pick() {
731+
let tokens = quote::quote!(CreateUser from Model, omit_default, pick = ["name", "email"]);
732+
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
733+
assert!(input.omit_default);
734+
assert_eq!(input.pick.unwrap(), vec!["name", "email"]);
735+
}
736+
737+
#[test]
738+
fn test_parse_schema_type_input_omit_default_defaults_to_false() {
739+
let tokens = quote::quote!(CreateUser from User);
740+
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
741+
assert!(!input.omit_default);
742+
}
702743
}

crates/vespera_macro/src/schema_macro/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,14 @@ pub fn generate_schema_type_code(
238238
continue;
239239
}
240240

241+
// Apply omit_default: skip fields with sea_orm(default_value) or sea_orm(primary_key)
242+
if input.omit_default
243+
&& (extract_sea_orm_default_value(&field.attrs).is_some()
244+
|| has_sea_orm_primary_key(&field.attrs))
245+
{
246+
continue;
247+
}
248+
241249
// Check if this is a SeaORM relation type
242250
let is_relation = is_seaorm_relation_type(&field.ty);
243251

crates/vespera_macro/src/schema_macro/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ fn test_generate_schema_type_code_preserves_struct_doc() {
575575
ignore_schema: false,
576576
rename_all: None,
577577
multipart: false,
578+
omit_default: false,
578579
};
579580
let struct_def = StructMetadata {
580581
name: "User".to_string(),

examples/axum-example/openapi.json

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2494,18 +2494,20 @@
24942494
},
24952495
"CreateUuidItemRequest": {
24962496
"type": "object",
2497+
"description": "UUID item model for testing UUID format in OpenAPI",
24972498
"properties": {
2498-
"external_ref": {
2499+
"externalRef": {
24992500
"type": "string",
25002501
"format": "uuid",
2502+
"description": "External reference UUID",
25012503
"nullable": true
25022504
},
25032505
"name": {
2504-
"type": "string"
2506+
"type": "string",
2507+
"description": "Item name"
25052508
},
25062509
"tags": {
25072510
"type": "array",
2508-
"description": "Unique tags for this item",
25092511
"items": {
25102512
"type": "string"
25112513
},
@@ -3986,22 +3988,26 @@
39863988
},
39873989
"UuidItem": {
39883990
"type": "object",
3991+
"description": "UUID item model for testing UUID format in OpenAPI",
39893992
"properties": {
3990-
"external_ref": {
3993+
"externalRef": {
39913994
"type": "string",
39923995
"format": "uuid",
3996+
"description": "External reference UUID",
39933997
"nullable": true
39943998
},
39953999
"id": {
39964000
"type": "string",
3997-
"format": "uuid"
4001+
"format": "uuid",
4002+
"description": "Item ID",
4003+
"default": "00000000-0000-0000-0000-000000000000"
39984004
},
39994005
"name": {
4000-
"type": "string"
4006+
"type": "string",
4007+
"description": "Item name"
40014008
},
40024009
"tags": {
40034010
"type": "array",
4004-
"description": "Unique tags for this item",
40054011
"items": {
40064012
"type": "string"
40074013
},

examples/axum-example/src/routes/uuid_items.rs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,10 @@
11
use std::collections::BTreeSet;
22

3-
use serde::{Deserialize, Serialize};
43
use uuid::Uuid;
5-
use vespera::Schema;
6-
use vespera::axum::Json;
4+
use vespera::{axum::Json, schema_type};
75

8-
#[derive(Serialize, Deserialize, Schema)]
9-
pub struct UuidItem {
10-
pub id: Uuid,
11-
pub name: String,
12-
pub external_ref: Option<Uuid>,
13-
/// Unique tags for this item
14-
pub tags: BTreeSet<String>,
15-
}
16-
17-
#[derive(Deserialize, Schema)]
18-
pub struct CreateUuidItemRequest {
19-
pub name: String,
20-
pub external_ref: Option<Uuid>,
21-
/// Unique tags for this item
22-
pub tags: BTreeSet<String>,
23-
}
6+
schema_type!(UuidItem from crate::models::uuid_item::Model, omit = ["created_at"], add = [("tags": BTreeSet<String>)]);
7+
schema_type!(CreateUuidItemRequest from crate::models::uuid_item::Model, omit_default, add = [("tags": BTreeSet<String>)]);
248

259
/// List all UUID items
2610
#[vespera::route(get, tags = ["uuid_items"])]

0 commit comments

Comments
 (0)