Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@

- [Quick Start](./getting-started/quick-start.md)
- [Installation](./getting-started/installation.md)
- [Attributes Reference](./getting-started/attributes.md)
- [Client Libraries](./clients/README.md)
86 changes: 86 additions & 0 deletions docs/src/getting-started/attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Attributes Reference

ReflectAPI provides `#[reflectapi(...)]` attributes that control how Rust types are reflected into the schema and generated clients.

## Struct / Enum Level

| Attribute | Description |
|-----------|-------------|
| `#[reflectapi(derive(...))]` | Forward additional derive traits to the generated Rust client type. |

## Field Level

### Type Override

| Attribute | Description |
|-----------|-------------|
| `#[reflectapi(type = "T")]` | Override the reflected type for both input and output schemas. |
| `#[reflectapi(input_type = "T")]` | Override the reflected type for the input schema only. |
| `#[reflectapi(output_type = "T")]` | Override the reflected type for the output schema only. |

### Transform

| Attribute | Description |
|-----------|-------------|
| `#[reflectapi(transform = "path::to::fn")]` | Apply a type transformation callback for both schemas. |
| `#[reflectapi(input_transform = "path::to::fn")]` | Apply a type transformation callback for input only. |
| `#[reflectapi(output_transform = "path::to::fn")]` | Apply a type transformation callback for output only. |

### Visibility

| Attribute | Description |
|-----------|-------------|
| `#[reflectapi(skip)]` | Exclude the field entirely from the schema. The field's type does not need to implement `Input`/`Output`. Equivalent to setting both `input_skip` and `output_skip`. |
| `#[reflectapi(input_skip)]` | Exclude the field from the input schema only. |
| `#[reflectapi(output_skip)]` | Exclude the field from the output schema only. |
| `#[reflectapi(hidden)]` | Keep the field in the schema (marked `"hidden": true`) but exclude it from generated clients, documentation, and OpenAPI specs. Useful for header fields that the server needs at runtime but clients should not see. |

### `skip` vs `hidden`

Both attributes remove a field from generated clients. The key difference:

- **`skip`** removes the field from the schema entirely. The field's type is never reflected, so it does not need to implement `Input` or `Output`. Use this for internal bookkeeping fields whose types are not part of your API.

- **`hidden`** keeps the field in the schema JSON (marked with `"hidden": true`) but excludes it from generated clients, documentation, and OpenAPI specs. The type must still implement the relevant trait. Use this for fields that are functionally required by server-side infrastructure — for example, a middleware or a proxy layer that populates the field / header before deserialization — but should not appear in client interfaces.

**When to use `hidden` over `skip`:** The field stays in the schema JSON so that server-side tooling (the axum adapter, middleware, or custom infrastructure) can inspect the full type structure at runtime. If nothing on the server needs the field's schema metadata, prefer `skip`.

Neither `skip` nor `hidden` affects serde serialization. For output types, serde will still serialize a hidden field onto the wire — `hidden` only controls what generated code and documentation show. If a field must never appear in responses, use `#[serde(skip_serializing)]` instead.

Please not that neither `skip` nor `hidden` prevent a malicious client from sending the fields in a request. A middleware may overwrite it or reject or validate as needed. It is up to the specific implementation of your server.

**Example: hidden header field**

```rust,ignore
#[derive(serde::Deserialize, reflectapi::Input)]
pub struct MyHeaders {
/// Visible to clients — they must provide this
pub authorization: String,

/// Not visible to the generated clients and documentation
/// Expected to be populated by a proxy or server-side middleware.
#[reflectapi(hidden)]
#[serde(default)]
pub x_internal_request_id: String,
}
```

### `#[serde(default)]` on skipped and hidden fields

When a field is excluded from generated clients (via `skip`, `input_skip`, or `hidden`), clients will not send it. Whether you add `#[serde(default)]` is your choice and depends on your deployment:

- **With `#[serde(default)]`:** If the field is absent, serde fills the default value. The request succeeds even if no proxy or middleware populates the field. Use this for optional metadata (trace IDs, correlation IDs) where absence is acceptable.

- **Without `#[serde(default)]`:** If the field is absent, deserialization fails with a protocol error. Use this for fields that a proxy or middleware is expected to inject — a missing value means the infrastructure is misconfigured, and you want to reject the request loudly rather than proceed silently with a zero-value.

### Restrictions

`#[reflectapi(hidden)]` cannot be used on unnamed (tuple) struct or enum variant fields. Hiding a positional element would shift indices in generated clients, breaking wire compatibility. Use `hidden` only on named fields.

## Enum Variant Level

| Attribute | Description |
|-----------|-------------|
| `#[reflectapi(skip)]` | Exclude the variant from the schema entirely. |
| `#[reflectapi(input_skip)]` | Exclude the variant from the input schema only. |
| `#[reflectapi(output_skip)]` | Exclude the variant from the output schema only. |
25 changes: 24 additions & 1 deletion reflectapi-demo/src/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ struct TestStructOneBasicFieldStringReflectBothEqually2 {
}
#[test]
fn test_reflectapi_struct_one_basic_field_string_reflectapi_both_equally2() {
assert_input_snapshot!(TestStructOneBasicFieldStringReflectBothEqually);
assert_input_snapshot!(TestStructOneBasicFieldStringReflectBothEqually2);
}

#[derive(reflectapi::Input, reflectapi::Output, serde::Deserialize, serde::Serialize)]
Expand Down Expand Up @@ -510,6 +510,7 @@ fn test_reflectapi_enum_with_skip_variant() {
#[derive(reflectapi::Input, reflectapi::Output, serde::Deserialize, serde::Serialize)]
struct TestStructWithSkipField {
#[reflectapi(skip)]
#[serde(default)]
_f: u8,
}

Expand All @@ -521,6 +522,7 @@ fn test_reflectapi_struct_with_skip_field() {
#[derive(reflectapi::Input, reflectapi::Output, serde::Deserialize, serde::Serialize)]
struct TestStructWithSkipFieldInput {
#[reflectapi(input_skip)]
#[serde(default)]
_f: u8,
}
#[test]
Expand Down Expand Up @@ -585,6 +587,7 @@ fn test_reflectapi_struct_with_additional_derives() {
Hash,
Default,
)]
#[allow(clippy::duplicated_attributes)]
#[reflectapi(derive(
Clone,
PartialOrd,
Expand Down Expand Up @@ -643,3 +646,23 @@ struct TestStructWithExternalGenericTypeFallback {
fn test_reflectapi_struct_with_external_generic_type_fallback() {
assert_snapshot!(TestStructWithExternalGenericTypeFallback);
}

#[test]
fn test_reflectapi_struct_with_hidden_header_field() {
#[derive(serde::Deserialize, reflectapi::Input)]
struct HeadersWithHidden {
/// Authorization header
_authorization: String,
/// Internal tracking header, hidden from clients
#[reflectapi(hidden)]
#[serde(default)]
_x_internal_trace_id: String,
}

assert_builder_snapshot!(reflectapi::Builder::<()>::new()
Comment thread
avkonst marked this conversation as resolved.
.name("hidden_header_test")
.route(
|_: (), _: reflectapi::Empty, _h: HeadersWithHidden| async { reflectapi::Empty {} },
|b| b.name("test.endpoint")
))
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading