fix: add default support - #89
Conversation
There was a problem hiding this comment.
Pull request overview
Adds end-to-end support for OpenAPI default values during lowering and code emission, so missing optional fields/params can deserialize into concrete Rust values (and error out when a default cannot be represented).
Changes:
- Lower schema and query-parameter
defaultinto an IRDefaultValue, and adjust optionality so “optional + default” becomes non-Option<T>(exceptnullable). - Emit
#[serde(default = "...")]plus per-struct associated default fns, and introduce a dedicatedUnsupportedDefaulterror for unrenderable defaults. - Add fixtures + generated outputs + coverage table updates documenting and testing supported/unsupported defaults.
Reviewed changes
Copilot reviewed 17 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/design.md | Documents the new default/Option lowering rules and edge cases (nullable, required fields, unrepresentable defaults). |
| crates/oapi-codegen/tests/generated/server_query_params.rs | Updates generated query struct to use a serde default fn and non-optional limit. |
| crates/oapi-codegen/tests/generated/schema_defaults.rs | New generated output exercising defaults across scalar/enum/empty-collection/nullable cases. |
| crates/oapi-codegen/tests/generated.rs | Registers the new generated module. |
| crates/oapi-codegen/tests/fixtures/unsupported_default_value.yaml | New fixture covering an unrepresentable default (non-empty array). |
| crates/oapi-codegen/tests/fixtures/server_query_params.yaml | Adds a query-parameter default used to drive generation changes. |
| crates/oapi-codegen/tests/fixtures/schema_defaults.yaml | New fixture covering supported default shapes and semantics. |
| crates/oapi-codegen/tests/coverage.rs | Marks meta.default supported and adds an explicit unsupported sub-feature + fixtures; wires new generated test. |
| crates/oapi-codegen/src/lower/schema.rs | Computes declared defaults for object properties, adjusts optionality rules, and lowers defaults into IR fields. |
| crates/oapi-codegen/src/lower/recurse.rs | Updates test field construction for the new Field.default member. |
| crates/oapi-codegen/src/lower/paths.rs | Adds query-parameter default lowering and threads owner name for generated default fn paths. |
| crates/oapi-codegen/src/lower/mod.rs | Exposes new lower::default module. |
| crates/oapi-codegen/src/lower/default.rs | New lowering module that validates/normalizes JSON defaults against Rust types and enum wire values. |
| crates/oapi-codegen/src/ir.rs | Extends Field with default: Option<DefaultValue> and defines DefaultValue. |
| crates/oapi-codegen/src/error.rs | Adds Error::UnsupportedDefault with display + error trait integration. |
| crates/oapi-codegen/src/emit/usage.rs | Updates test helper Field construction for the new default member. |
| crates/oapi-codegen/src/emit/models.rs | Emits serde default attributes + associated default fns and renders DefaultValue into Rust expressions. |
| crates/oapi-codegen/src/console.rs | Routes UnsupportedDefault into hint printing. |
| crates/oapi-codegen/Cargo.toml | Adds serde_json dependency for default handling. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (2)
crates/oapi-codegen/src/lower/default.rs:64
lower_defaulttreats bothi32andi64defaults viaValue::as_i64()with no range check. For anint32field, a schema default outside thei32range will currently lower successfully and then fail later as a Rust compile error (literal/type mismatch). Prefer rejecting out-of-range values here so the generator reports a clearUnsupportedDefaulterror instead of producing uncompilable output.
RustType::Bool => json.as_bool().map(DefaultValue::Bool),
RustType::I32 | RustType::I64 => json.as_i64().map(DefaultValue::Int),
docs/design.md:310
- The example
?limit=is misleading:#[serde(default = ...)]only applies when the parameter is absent, not when it is present but empty. Withaxum_extra::extract::Query/serde_urlencoded,limit=will typically fail to deserialize intoi32rather than using the default. The text should describe omittinglimitentirely.
A query parameter follows the same rule. `?limit=` with `default: 20` gives a
plain `i32` field.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (3)
docs/design.md:310
- The example
?limit=is a present-but-empty query parameter, not an absent parameter. A serde default only applies when the key is missing; with?limit=deserialization ofi32will typically fail rather than falling back to the default. Reword this to describe an absentlimitparameter (or use an example that omitslimitentirely).
A query parameter follows the same rule. `?limit=` with `default: 20` gives a
plain `i32` field.
crates/oapi-codegen/Cargo.toml:27
serde_jsonis now a normal dependency (needed by non-test code), but it is still listed in[dev-dependencies]as well. Keeping the same crate/version in both places is redundant and can drift over time; tests can use the normal dependency.
serde_json = "1.0.151"
crates/oapi-codegen/src/emit/models.rs:180
- Default functions are emitted whenever
field.defaultis present, even when the struct does not derive any serde traits (has_serde == false). In that caseemit_fieldsuppresses the#[serde(default = "...")]attribute, so the generateddefault_*functions become unused and can trigger dead_code warnings in the generated crate. Gate emitting default functions (and theimplblock) onhas_serde.
let mut defaults = Vec::new();
for field in &strukt.fields {
fields.push(emit_field(field, has_serde, &strukt.name)?);
if let Some(value) = &field.default {
defaults.push(emit_default_fn(field, value)?);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (2)
crates/oapi-codegen/src/emit/models.rs:246
- The generated doc string for default helper functions has a leading space (
" The ..."), which will be rendered into rustdoc output and looks accidental. Removing it keeps documentation formatting clean.
let doc = format!(" The `default` the document gives `{}`.", field.name.logical());
crates/oapi-codegen/src/lower/mod.rs:8
loweris exported from the crate (pub mod lowerin lib.rs), so makinglower::defaultpublic also exposes this new internal module as part of the public API. Iflower_defaultis only an implementation detail, keep the module private to avoid committing to it as a supported API surface.
pub mod default;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (3)
crates/oapi-codegen/src/lower/default.rs:47
- The
hintstring forUnsupportedDefaultuses line-continuation (\) with indentation, which will embed large runs of whitespace in the emitted hint text. This makes the console hint harder to read/copy.
hint: format!(
"`{owner}.{property}` lowers to {}. Give it a `default` of that type, or remove the `default`. \
This generator renders a scalar, an enum value, an empty array, and an empty object. \
It cannot render a non-empty array or object.",
describe(ty)
crates/oapi-codegen/src/lower/paths.rs:456
query_fieldalready resolves the parameter schema insidequery_param_type, butquery_param_defaultresolves/clones the schema again just to readschema_data.default. For inline (non-$ref) parameter schemas, this extra clone/work is avoidable.
/// The `default` a query parameter's schema declares, if any.
fn query_param_default(
&self,
path: &str,
method: &str,
origin: Option<&str>,
data: &ParameterData,
) -> Result<Option<serde_json::Value>> {
let ParameterSchemaOrContent::Schema(schema) = &data.format else {
// `query_param_type` rejects a `content` parameter with a better
// message than this could give.
return Ok(None);
};
let schema = self.resolve_param_schema(path, method, origin, &data.name, schema)?;
return Ok(schema.schema_data.default);
}
docs/design.md:306
- The design doc implies that an “empty object” default works for any object-typed field, but the implementation only supports
default: {}when the field lowers to aHashMap(RustType::Map). For struct-shaped objects (named types),{}has no literal form and will be rejected as an unsupported default. Clarifying this avoids misleading spec authors.
Only a value with a literal form works: a string, a number, a boolean, an enum
value, an empty array, and an empty object. A non-empty array, a non-empty
object, and a value of the wrong type are errors, not silent drops. A dropped
|
🎉 This PR is included in version 1.0.0-dev.27 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
|
🎉 This PR is included in version 1.0.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
No description provided.