Skip to content

Commit

Permalink
Allow value_type serde_json::Value (juhaku#568)
Browse files Browse the repository at this point in the history
Add support for `"AnyType"` according to https://swagger.io/docs/specification/data-models/data-types/. This is achieved by adding a new virtual type called `Value`. When `Value` 
is used as type, does not have any type restrictions in OpenAPI. 
The `Value` type can be used along with `serde_json::Value` to allow 
usage of dynamic content. 

Also add a new example `raw-json-actix` to demonstrate the 
newly added `Value` virtual type and update docs.
  • Loading branch information
jayvdb authored Apr 14, 2023
1 parent 892bd61 commit c0c1470
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 22 deletions.
20 changes: 20 additions & 0 deletions examples/raw-json-actix/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "raw-json-actix"
description = "Simple actix-web using raw JSON with utoipa and Swagger"
version = "0.1.0"
edition = "2021"
license = "MIT"
authors = [
"Example <[email protected]>"
]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "4"
env_logger = "0.10.0"
serde_json = "1.0"
utoipa = { path = "../../utoipa", features = ["actix_extras"] }
utoipa-swagger-ui = { path = "../../utoipa-swagger-ui", features = ["actix-web"] }

[workspace]
15 changes: 15 additions & 0 deletions examples/raw-json-actix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# raw-json-actix

This is demo `actix-web` application showing using raw JSON in endpoints.
The API demonstrates `utoipa` with `utoipa-swagger-ui` functionalities.

Just run command below to run the demo application and browse to `http://localhost:8080/swagger-ui/`.
```bash
cargo run
```

In the swagger UI:

1. Send body `"string"` and the console will show the body was a `serde_json::String`.
2. Send body `1` and the console will show the body was a `serde_json::Number`.
3. Send body `[1, 2]` and the console will show the body was a `serde_json::Array`.
48 changes: 48 additions & 0 deletions examples/raw-json-actix/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use std::{error::Error, net::Ipv4Addr};

use actix_web::{
middleware::Logger, patch, App, HttpResponse, HttpServer, Responder, Result, web::Json,
};
use serde_json::Value;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;

#[utoipa::path(
request_body = Value,
responses(
(status = 200, description = "Patch completed"),
(status = 406, description = "Not accepted"),
),
security(
("api_key" = [])
),
)]
#[patch("/patch_raw")]
pub async fn patch_raw(body: Json<Value>) -> Result<impl Responder> {
let value: Value = body.into_inner();
eprintln!("body = {:?}", value);
Ok(HttpResponse::Ok())
}

#[actix_web::main]
async fn main() -> Result<(), impl Error> {
env_logger::init();

#[derive(OpenApi)]
#[openapi(paths(patch_raw))]
struct ApiDoc;

let openapi = ApiDoc::openapi();

HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.service(patch_raw)
.service(
SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()),
)
})
.bind((Ipv4Addr::UNSPECIFIED, 8080))?
.run()
.await
}
22 changes: 20 additions & 2 deletions utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,14 @@ impl<'t> TypeTree<'t> {

fn convert(path: &'t Path, last_segment: &'t PathSegment) -> TypeTree<'t> {
let generic_type = Self::get_generic_type(last_segment);
let is_primitive = SchemaType(path).is_primitive();
let schema_type = SchemaType(path);

Self {
path: Some(Cow::Borrowed(path)),
value_type: if is_primitive {
value_type: if schema_type.is_primitive() {
ValueType::Primitive
} else if schema_type.is_value() {
ValueType::Value
} else {
ValueType::Object
},
Expand Down Expand Up @@ -321,6 +323,12 @@ impl<'t> TypeTree<'t> {
self.is("Object")
}

/// `Value` virtual type is used when any JSON value is required in OpenAPI spec. Typically used
/// with `value_type` attribute for a member of type `serde_json::Value`.
pub fn is_value(&self) -> bool {
self.is("Value")
}

/// Check whether the [`TypeTree`]'s `generic_type` is [`GenericType::Option`]
pub fn is_option(&self) -> bool {
matches!(self.generic_type, Some(GenericType::Option))
Expand Down Expand Up @@ -356,6 +364,7 @@ pub enum ValueType {
Primitive,
Object,
Tuple,
Value,
}

#[cfg_attr(feature = "debug", derive(Debug))]
Expand Down Expand Up @@ -679,6 +688,15 @@ impl<'c> ComponentSchema {
tokens.extend(features.to_token_stream());
nullable.to_tokens(tokens);
}
ValueType::Value => {
if type_tree.is_value() {
tokens.extend(quote! {
utoipa::openapi::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::SchemaType::Value)
#description_stream #deprecated_stream #nullable
})
}
}
ValueType::Object => {
let is_inline = features.is_inline();

Expand Down
2 changes: 1 addition & 1 deletion utoipa-gen/src/ext/actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fn get_primitive_args(value_args: Vec<FnArg>) -> impl Iterator<Item = TypeTree>
ValueType::Tuple => path_arg
.children
.expect("ValueType::Tuple will always have children"),
ValueType::Object => {
ValueType::Object | ValueType::Value => {
unreachable!("Value arguments does not have ValueType::Object arguments")
}
})
Expand Down
18 changes: 12 additions & 6 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ use self::{
/// * `value_type = ...` Can be used to override default type derived from type of the field used in OpenAPI spec.
/// This is useful in cases where the default type does not correspond to the actual type e.g. when
/// any third-party types are used which are not [`ToSchema`][to_schema]s nor [`primitive` types][primitive].
/// Value can be any Rust type what normally could be used to serialize to JSON or custom type such as _`Object`_.
/// _`Object`_ will be rendered as generic OpenAPI object _(`type: object`)_.
/// The value can be any Rust type what normally could be used to serialize to JSON or either virtual type _`Object`_
/// or _`Value`.
/// _`Object`_ will be rendered as generic OpenAPI object _(`type: object`)_.
/// _`Value`_ will be rendered as any OpenAPI value (i.e. no `type` restriction).
/// * `title = ...` Literal string value. Can be used to define title for struct in OpenAPI
/// document. Some OpenAPI code generation libraries also use this field as a name for the
/// struct.
Expand All @@ -146,8 +148,10 @@ use self::{
/// * `value_type = ...` Can be used to override default type derived from type of the field used in OpenAPI spec.
/// This is useful in cases where the default type does not correspond to the actual type e.g. when
/// any third-party types are used which are not [`ToSchema`][to_schema]s nor [`primitive` types][primitive].
/// Value can be any Rust type what normally could be used to serialize to JSON or custom type such as _`Object`_.
/// _`Object`_ will be rendered as generic OpenAPI object _(`type: object`)_.
/// The value can be any Rust type what normally could be used to serialize to JSON, or either virtual type _`Object`_
/// or _`Value`.
/// _`Object`_ will be rendered as generic OpenAPI object _(`type: object`)_.
/// _`Value`_ will be rendered as any OpenAPI value (i.e. no `type` restriction).
/// * `inline` If the type of this field implements [`ToSchema`][to_schema], then the schema definition
/// will be inlined. **warning:** Don't use this for recursive data types!
/// * `required = ...` Can be used to enforce required status for the field. [See
Expand Down Expand Up @@ -1636,8 +1640,10 @@ pub fn openapi(input: TokenStream) -> TokenStream {
/// * `value_type = ...` Can be used to override default type derived from type of the field used in OpenAPI spec.
/// This is useful in cases where the default type does not correspond to the actual type e.g. when
/// any third-party types are used which are not [`ToSchema`][to_schema]s nor [`primitive` types][primitive].
/// Value can be any Rust type what normally could be used to serialize to JSON or custom type such as _`Object`_.
/// _`Object`_ will be rendered as generic OpenAPI object.
/// The value can be any Rust type what normally could be used to serialize to JSON, or either virtual type _`Object`_
/// or _`Value`.
/// _`Object`_ will be rendered as generic OpenAPI object _(`type: object`)_.
/// _`Value`_ will be rendered as any OpenAPI value (i.e. no `type` restriction).
///
/// * `inline` If set, the schema for this field's type needs to be a [`ToSchema`][to_schema], and
/// the schema definition will be inlined.
Expand Down
4 changes: 4 additions & 0 deletions utoipa-gen/src/schema_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ impl SchemaType<'_> {
.to_string()
}

pub fn is_value(&self) -> bool {
matches!(&*self.last_segment_to_string(), "Value")
}

/// Check whether type is known to be primitive in which case returns true.
pub fn is_primitive(&self) -> bool {
let SchemaType(path) = self;
Expand Down
16 changes: 8 additions & 8 deletions utoipa-gen/tests/request_body_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,16 +496,16 @@ fn unit_type_request_body() {
fn request_body_with_example() {
#[derive(ToSchema)]
#[allow(unused)]
struct Value<'v> {
struct Foo<'v> {
value: &'v str,
}

#[utoipa::path(get, path = "/item", request_body(content = Value, example = json!({"value": "this is value"})))]
#[utoipa::path(get, path = "/item", request_body(content = Foo, example = json!({"value": "this is value"})))]
#[allow(dead_code)]
fn get_item() {}

#[derive(OpenApi)]
#[openapi(components(schemas(Value)), paths(get_item))]
#[openapi(components(schemas(Foo)), paths(get_item))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
Expand All @@ -521,7 +521,7 @@ fn request_body_with_example() {
"value": "this is value"
},
"schema": {
"$ref": "#/components/schemas/Value"
"$ref": "#/components/schemas/Foo"
}
}
})
Expand All @@ -532,14 +532,14 @@ fn request_body_with_example() {
fn request_body_with_examples() {
#[derive(ToSchema)]
#[allow(unused)]
struct Value<'v> {
struct Foo<'v> {
value: &'v str,
}

#[utoipa::path(
get,
path = "/item",
request_body(content = Value,
request_body(content = Foo,
examples(
("Value1" = (value = json!({"value": "this is value"}) ) ),
("Value2" = (value = json!({"value": "this is value2"}) ) )
Expand All @@ -550,7 +550,7 @@ fn request_body_with_examples() {
fn get_item() {}

#[derive(OpenApi)]
#[openapi(components(schemas(Value)), paths(get_item))]
#[openapi(components(schemas(Foo)), paths(get_item))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
Expand All @@ -575,7 +575,7 @@ fn request_body_with_examples() {
}
},
"schema": {
"$ref": "#/components/schemas/Value"
"$ref": "#/components/schemas/Foo"
}
}
})
Expand Down
43 changes: 43 additions & 0 deletions utoipa-gen/tests/schema_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4233,6 +4233,49 @@ fn derive_schema_with_object_type_description() {
)
}

#[test]
fn derive_schema_with_explicit_value_type() {
let value = api_doc! {
struct Value {
#[schema(value_type = Value)]
any: String,
}
};

assert_json_eq!(
value,
json!({
"properties": {
"any": {
},
},
"required": ["any"],
"type": "object"
})
)
}

#[test]
fn derive_schema_with_implicit_value_type() {
let value = api_doc! {
struct Value {
any: serde_json::Value,
}
};

assert_json_eq!(
value,
json!({
"properties": {
"any": {
},
},
"required": ["any"],
"type": "object"
})
)
}

#[test]
fn derive_tuple_named_struct_field() {
#[derive(ToSchema)]
Expand Down
8 changes: 4 additions & 4 deletions utoipa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
//!
//! # Examples
//!
//! Create a struct or it could be an enum also. Add `ToSchema` derive macro to it so it can be registered
//! Create a struct, or it could be an enum also. Add `ToSchema` derive macro to it so it can be registered
//! as a component in openapi schema.
//! ```rust
//! use utoipa::ToSchema;
Expand All @@ -118,7 +118,6 @@
//! Create an handler that would handle your business logic and add `path` proc attribute macro over it.
//! ```rust
//! mod pet_api {
//! # use utoipa::OpenApi;
//! # use utoipa::ToSchema;
//! #
//! # #[derive(ToSchema)]
Expand Down Expand Up @@ -150,9 +149,10 @@
//! }
//! }
//! ```
//!
//! Utoipa has support for [http](https://crates.io/crates/http) `StatusCode` in responses.
//!
//! Tie the component and the above api to the openapi schema with following `OpenApi` derive proc macro.
//! Tie the above component and api to the openapi schema with following `OpenApi` derive proc macro.
//! ```rust
//! # mod pet_api {
//! # use utoipa::ToSchema;
Expand Down Expand Up @@ -194,7 +194,7 @@
//! # name: String,
//! # age: Option<i32>,
//! # }
//! # use utoipa::OpenApi;
//! use utoipa::OpenApi;
//! #[derive(OpenApi)]
//! #[openapi(paths(pet_api::get_pet_by_id), components(schemas(Pet)))]
//! struct ApiDoc;
Expand Down
10 changes: 9 additions & 1 deletion utoipa/src/openapi/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ builder! {
pub struct Object {
/// Type of [`Object`] e.g. [`SchemaType::Object`] for `object` and [`SchemaType::String`] for
/// `string` types.
#[serde(rename = "type")]
#[serde(rename = "type", skip_serializing_if="SchemaType::is_value")]
pub schema_type: SchemaType,

/// Changes the [`Object`] title.
Expand Down Expand Up @@ -1135,6 +1135,9 @@ pub enum SchemaType {
/// Used with [`Object`] and [`ObjectBuilder`]. Objects always have
/// _schema_type_ [`SchemaType::Object`].
Object,
/// Indicates generic JSON content. Used with [`Object`] and [`ObjectBuilder`] on a field
/// of any valid JSON type.
Value,
/// Indicates string type of content. Used with [`Object`] and [`ObjectBuilder`] on a `string`
/// field.
String,
Expand All @@ -1150,6 +1153,11 @@ pub enum SchemaType {
/// Used with [`Array`] and [`ArrayBuilder`]. Indicates array type of content.
Array,
}
impl SchemaType {
fn is_value(type_: &SchemaType) -> bool {
*type_ == SchemaType::Value
}
}

impl Default for SchemaType {
fn default() -> Self {
Expand Down

0 comments on commit c0c1470

Please sign in to comment.