Skip to main content

openapi_to_rust/
openapi.rs

1use once_cell::sync::Lazy;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::BTreeMap;
5
6#[derive(Debug, Clone, Deserialize, Serialize)]
7pub struct OpenApiSpec {
8    pub openapi: String,
9    pub info: Info,
10    pub paths: Option<BTreeMap<String, PathItem>>,
11    pub components: Option<Components>,
12    #[serde(flatten)]
13    pub extra: BTreeMap<String, Value>,
14}
15
16#[derive(Debug, Clone, Deserialize, Serialize)]
17pub struct Info {
18    pub title: String,
19    #[serde(default)]
20    pub version: Option<String>,
21    #[serde(flatten)]
22    pub extra: BTreeMap<String, Value>,
23}
24
25#[derive(Debug, Clone, Deserialize, Serialize)]
26pub struct Components {
27    pub schemas: Option<BTreeMap<String, Schema>>,
28    pub parameters: Option<BTreeMap<String, Parameter>>,
29    #[serde(flatten)]
30    pub extra: BTreeMap<String, Value>,
31}
32
33#[derive(Debug, Clone, Deserialize, Serialize)]
34#[serde(untagged)]
35pub enum Schema {
36    /// Schema reference
37    Reference {
38        #[serde(rename = "$ref")]
39        reference: String,
40        #[serde(flatten)]
41        extra: BTreeMap<String, Value>,
42    },
43    /// Recursive reference (OpenAPI 3.1)
44    RecursiveRef {
45        #[serde(rename = "$recursiveRef")]
46        recursive_ref: String,
47        #[serde(flatten)]
48        extra: BTreeMap<String, Value>,
49    },
50    /// OneOf union
51    OneOf {
52        #[serde(rename = "oneOf")]
53        one_of: Vec<Schema>,
54        discriminator: Option<Discriminator>,
55        #[serde(flatten)]
56        details: SchemaDetails,
57    },
58    /// AnyOf union (must come before Typed to handle type + anyOf patterns)
59    AnyOf {
60        #[serde(rename = "type")]
61        schema_type: Option<SchemaType>,
62        #[serde(rename = "anyOf")]
63        any_of: Vec<Schema>,
64        discriminator: Option<Discriminator>,
65        #[serde(flatten)]
66        details: SchemaDetails,
67    },
68    /// Schema with explicit type
69    Typed {
70        #[serde(rename = "type")]
71        schema_type: SchemaType,
72        #[serde(flatten)]
73        details: SchemaDetails,
74    },
75    /// AllOf composition
76    AllOf {
77        #[serde(rename = "allOf")]
78        all_of: Vec<Schema>,
79        #[serde(flatten)]
80        details: SchemaDetails,
81    },
82    /// Schema without explicit type (inferred from other fields)
83    Untyped {
84        #[serde(flatten)]
85        details: SchemaDetails,
86    },
87}
88
89#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
90#[serde(rename_all = "lowercase")]
91pub enum SchemaType {
92    String,
93    Integer,
94    Number,
95    Boolean,
96    Array,
97    Object,
98    #[serde(rename = "null")]
99    Null,
100}
101
102#[derive(Debug, Clone, Deserialize, Serialize)]
103pub struct SchemaDetails {
104    pub description: Option<String>,
105    pub nullable: Option<bool>,
106
107    // OpenAPI 3.1 recursive support
108    #[serde(rename = "$recursiveAnchor")]
109    pub recursive_anchor: Option<bool>,
110
111    // String-specific
112    #[serde(rename = "enum")]
113    pub enum_values: Option<Vec<Value>>,
114    pub format: Option<String>,
115    pub default: Option<Value>,
116    #[serde(rename = "const")]
117    pub const_value: Option<Value>,
118
119    // Object-specific
120    pub properties: Option<BTreeMap<String, Schema>>,
121    pub required: Option<Vec<String>>,
122    #[serde(rename = "additionalProperties")]
123    pub additional_properties: Option<AdditionalProperties>,
124
125    // Array-specific
126    pub items: Option<Box<Schema>>,
127
128    // Number-specific
129    pub minimum: Option<f64>,
130    pub maximum: Option<f64>,
131
132    // Validation
133    #[serde(rename = "minLength")]
134    pub min_length: Option<u64>,
135    #[serde(rename = "maxLength")]
136    pub max_length: Option<u64>,
137    pub pattern: Option<String>,
138
139    // Extensions and unknown fields
140    #[serde(flatten)]
141    pub extra: BTreeMap<String, Value>,
142}
143
144#[derive(Debug, Clone, Deserialize, Serialize)]
145#[serde(untagged)]
146pub enum AdditionalProperties {
147    Boolean(bool),
148    Schema(Box<Schema>),
149}
150
151#[derive(Debug, Clone, Deserialize, Serialize)]
152pub struct Discriminator {
153    #[serde(rename = "propertyName")]
154    pub property_name: String,
155    pub mapping: Option<BTreeMap<String, String>>,
156    #[serde(flatten)]
157    pub extra: BTreeMap<String, Value>,
158}
159
160impl Schema {
161    /// Get the schema type if explicitly set
162    pub fn schema_type(&self) -> Option<&SchemaType> {
163        match self {
164            Schema::Typed { schema_type, .. } => Some(schema_type),
165            _ => None,
166        }
167    }
168
169    /// Get schema details
170    pub fn details(&self) -> &SchemaDetails {
171        match self {
172            Schema::Typed { details, .. } => details,
173            Schema::Reference { .. } => {
174                static EMPTY_DETAILS: Lazy<SchemaDetails> = Lazy::new(|| SchemaDetails {
175                    description: None,
176                    nullable: None,
177                    recursive_anchor: None,
178                    enum_values: None,
179                    format: None,
180                    default: None,
181                    const_value: None,
182                    properties: None,
183                    required: None,
184                    additional_properties: None,
185                    items: None,
186                    minimum: None,
187                    maximum: None,
188                    min_length: None,
189                    max_length: None,
190                    pattern: None,
191                    extra: BTreeMap::new(),
192                });
193                &EMPTY_DETAILS
194            }
195            Schema::RecursiveRef { .. } => {
196                static EMPTY_DETAILS_RECURSIVE: Lazy<SchemaDetails> = Lazy::new(|| SchemaDetails {
197                    description: None,
198                    nullable: None,
199                    recursive_anchor: None,
200                    enum_values: None,
201                    format: None,
202                    default: None,
203                    const_value: None,
204                    properties: None,
205                    required: None,
206                    additional_properties: None,
207                    items: None,
208                    minimum: None,
209                    maximum: None,
210                    min_length: None,
211                    max_length: None,
212                    pattern: None,
213                    extra: BTreeMap::new(),
214                });
215                &EMPTY_DETAILS_RECURSIVE
216            }
217            Schema::OneOf { details, .. } => details,
218            Schema::AnyOf { details, .. } => details,
219            Schema::AllOf { details, .. } => details,
220            Schema::Untyped { details } => details,
221        }
222    }
223
224    /// Get mutable schema details
225    pub fn details_mut(&mut self) -> &mut SchemaDetails {
226        match self {
227            Schema::Typed { details, .. } => details,
228            Schema::Reference { .. } => {
229                // Cannot mutate reference details
230                panic!("Cannot get mutable details for reference schema")
231            }
232            Schema::RecursiveRef { .. } => {
233                // Cannot mutate recursive reference details
234                panic!("Cannot get mutable details for recursive reference schema")
235            }
236            Schema::OneOf { details, .. } => details,
237            Schema::AnyOf { details, .. } => details,
238            Schema::AllOf { details, .. } => details,
239            Schema::Untyped { details } => details,
240        }
241    }
242
243    /// Check if this is any kind of reference (regular or recursive)
244    pub fn is_reference(&self) -> bool {
245        matches!(self, Schema::Reference { .. } | Schema::RecursiveRef { .. })
246    }
247
248    /// Get reference string if this is a reference
249    pub fn reference(&self) -> Option<&str> {
250        match self {
251            Schema::Reference { reference, .. } => Some(reference),
252            _ => None,
253        }
254    }
255
256    /// Get recursive reference string if this is a recursive reference
257    pub fn recursive_reference(&self) -> Option<&str> {
258        match self {
259            Schema::RecursiveRef { recursive_ref, .. } => Some(recursive_ref),
260            _ => None,
261        }
262    }
263
264    /// Check if this is a discriminated union
265    pub fn is_discriminated_union(&self) -> bool {
266        match self {
267            Schema::OneOf { discriminator, .. } => discriminator.is_some(),
268            Schema::AnyOf { discriminator, .. } => discriminator.is_some(),
269            _ => false,
270        }
271    }
272
273    /// Get discriminator if this is a discriminated union
274    pub fn discriminator(&self) -> Option<&Discriminator> {
275        match self {
276            Schema::OneOf { discriminator, .. } => discriminator.as_ref(),
277            Schema::AnyOf { discriminator, .. } => discriminator.as_ref(),
278            _ => None,
279        }
280    }
281
282    /// Get union variants
283    pub fn union_variants(&self) -> Option<&[Schema]> {
284        match self {
285            Schema::OneOf { one_of, .. } => Some(one_of),
286            Schema::AnyOf { any_of, .. } => Some(any_of),
287            _ => None,
288        }
289    }
290
291    /// Check if this appears to be a nullable pattern (anyOf with null)
292    pub fn is_nullable_pattern(&self) -> bool {
293        match self {
294            Schema::AnyOf { any_of, .. } => {
295                any_of.len() == 2
296                    && any_of
297                        .iter()
298                        .any(|s| matches!(s.schema_type(), Some(SchemaType::Null)))
299            }
300            _ => false,
301        }
302    }
303
304    /// Get the non-null variant from a nullable pattern
305    pub fn non_null_variant(&self) -> Option<&Schema> {
306        if self.is_nullable_pattern() {
307            if let Schema::AnyOf { any_of, .. } = self {
308                return any_of
309                    .iter()
310                    .find(|s| !matches!(s.schema_type(), Some(SchemaType::Null)));
311            }
312        }
313        None
314    }
315
316    /// Infer schema type from structure if not explicitly set
317    pub fn inferred_type(&self) -> Option<SchemaType> {
318        match self {
319            Schema::Typed { schema_type, .. } => Some(schema_type.clone()),
320            Schema::Untyped { details } => {
321                // Infer from structure
322                if details.properties.is_some() {
323                    Some(SchemaType::Object)
324                } else if details.items.is_some() {
325                    Some(SchemaType::Array)
326                } else if details.enum_values.is_some() {
327                    Some(SchemaType::String) // Assume string enum
328                } else {
329                    None
330                }
331            }
332            _ => None,
333        }
334    }
335}
336
337impl SchemaDetails {
338    /// Check if this schema is nullable
339    pub fn is_nullable(&self) -> bool {
340        self.nullable.unwrap_or(false)
341    }
342
343    /// Check if this is a string enum
344    pub fn is_string_enum(&self) -> bool {
345        self.enum_values.is_some()
346    }
347
348    /// Get enum values as strings if this is a string enum
349    pub fn string_enum_values(&self) -> Option<Vec<String>> {
350        self.enum_values.as_ref().map(|values| {
351            values
352                .iter()
353                .filter_map(|v| v.as_str())
354                .map(|s| s.to_string())
355                .collect()
356        })
357    }
358
359    /// Check if a field is required
360    pub fn is_field_required(&self, field_name: &str) -> bool {
361        self.required
362            .as_ref()
363            .map(|req| req.contains(&field_name.to_string()))
364            .unwrap_or(false)
365    }
366}
367
368/// OpenAPI Path Item Object  
369#[derive(Debug, Clone, Deserialize, Serialize)]
370pub struct PathItem {
371    #[serde(rename = "get")]
372    pub get: Option<Operation>,
373    #[serde(rename = "put")]
374    pub put: Option<Operation>,
375    #[serde(rename = "post")]
376    pub post: Option<Operation>,
377    #[serde(rename = "delete")]
378    pub delete: Option<Operation>,
379    #[serde(rename = "options")]
380    pub options: Option<Operation>,
381    #[serde(rename = "head")]
382    pub head: Option<Operation>,
383    #[serde(rename = "patch")]
384    pub patch: Option<Operation>,
385    #[serde(rename = "trace")]
386    pub trace: Option<Operation>,
387    pub parameters: Option<Vec<Parameter>>,
388    #[serde(flatten)]
389    pub extra: BTreeMap<String, Value>,
390}
391
392impl PathItem {
393    /// Get all operations in this path item
394    pub fn operations(&self) -> Vec<(&str, &Operation)> {
395        let mut ops = Vec::new();
396        if let Some(ref op) = self.get {
397            ops.push(("get", op));
398        }
399        if let Some(ref op) = self.put {
400            ops.push(("put", op));
401        }
402        if let Some(ref op) = self.post {
403            ops.push(("post", op));
404        }
405        if let Some(ref op) = self.delete {
406            ops.push(("delete", op));
407        }
408        if let Some(ref op) = self.options {
409            ops.push(("options", op));
410        }
411        if let Some(ref op) = self.head {
412            ops.push(("head", op));
413        }
414        if let Some(ref op) = self.patch {
415            ops.push(("patch", op));
416        }
417        if let Some(ref op) = self.trace {
418            ops.push(("trace", op));
419        }
420        ops
421    }
422}
423
424/// OpenAPI Operation Object
425#[derive(Debug, Clone, Deserialize, Serialize)]
426pub struct Operation {
427    #[serde(rename = "operationId")]
428    pub operation_id: Option<String>,
429    pub summary: Option<String>,
430    pub description: Option<String>,
431    pub parameters: Option<Vec<Parameter>>,
432    #[serde(rename = "requestBody")]
433    pub request_body: Option<RequestBody>,
434    pub responses: Option<BTreeMap<String, Response>>,
435    #[serde(flatten)]
436    pub extra: BTreeMap<String, Value>,
437}
438
439/// OpenAPI Parameter Object
440#[derive(Debug, Clone, Deserialize, Serialize)]
441pub struct Parameter {
442    pub name: Option<String>,
443    #[serde(rename = "in")]
444    pub location: Option<String>,
445    pub required: Option<bool>,
446    pub schema: Option<Schema>,
447    pub description: Option<String>,
448    #[serde(flatten)]
449    pub extra: BTreeMap<String, Value>,
450}
451
452/// OpenAPI Request Body Object
453#[derive(Debug, Clone, Deserialize, Serialize)]
454pub struct RequestBody {
455    pub content: Option<BTreeMap<String, MediaType>>,
456    pub description: Option<String>,
457    pub required: Option<bool>,
458    #[serde(flatten)]
459    pub extra: BTreeMap<String, Value>,
460}
461
462/// Returns true for media types whose payload is JSON.
463///
464/// Matches `application/json` exactly, plus any RFC 6839 structured-syntax
465/// suffix variant of the form `application/<subtype>+json`
466/// (e.g. `application/vnd.api+json`, `application/hal+json`,
467/// `application/problem+json`). Trailing parameters such as
468/// `; charset=utf-8` are tolerated.
469pub fn is_json_media_type(ct: &str) -> bool {
470    let essence = ct
471        .split(';')
472        .next()
473        .unwrap_or(ct)
474        .trim()
475        .to_ascii_lowercase();
476    if essence == "application/json" {
477        return true;
478    }
479    if let Some(subtype) = essence.strip_prefix("application/") {
480        return subtype.ends_with("+json");
481    }
482    false
483}
484
485/// Returns true for `application/x-www-form-urlencoded` (with optional
486/// parameters).
487pub fn is_form_urlencoded_media_type(ct: &str) -> bool {
488    let essence = ct
489        .split(';')
490        .next()
491        .unwrap_or(ct)
492        .trim()
493        .to_ascii_lowercase();
494    essence == "application/x-www-form-urlencoded"
495}
496
497fn find_json_content(content: &BTreeMap<String, MediaType>) -> Option<(&str, &MediaType)> {
498    if let Some(mt) = content.get("application/json") {
499        return Some(("application/json", mt));
500    }
501    content
502        .iter()
503        .find(|(ct, _)| is_json_media_type(ct))
504        .map(|(ct, mt)| (ct.as_str(), mt))
505}
506
507impl RequestBody {
508    /// Get schema for any JSON content type
509    ///
510    /// Prefers the canonical `application/json` entry, then falls back to
511    /// any `application/*+json` variant (RFC 6839) such as
512    /// `application/vnd.api+json` or `application/hal+json`.
513    pub fn json_schema(&self) -> Option<&Schema> {
514        self.content
515            .as_ref()
516            .and_then(find_json_content)
517            .and_then(|(_, media_type)| media_type.schema.as_ref())
518    }
519
520    /// Get the best content type and its schema, preferring JSON over others
521    pub fn best_content(&self) -> Option<(&str, Option<&Schema>)> {
522        let content = self.content.as_ref()?;
523
524        if let Some((ct, media_type)) = find_json_content(content) {
525            return Some((ct, media_type.schema.as_ref()));
526        }
527
528        const PRIORITY: &[&str] = &[
529            "application/x-www-form-urlencoded",
530            "multipart/form-data",
531            "application/octet-stream",
532            "text/plain",
533        ];
534        for ct in PRIORITY {
535            if let Some(media_type) = content.get(*ct) {
536                return Some((*ct, media_type.schema.as_ref()));
537            }
538        }
539        None
540    }
541}
542
543/// OpenAPI Response Object
544#[derive(Debug, Clone, Deserialize, Serialize)]
545pub struct Response {
546    pub description: Option<String>,
547    pub content: Option<BTreeMap<String, MediaType>>,
548    #[serde(flatten)]
549    pub extra: BTreeMap<String, Value>,
550}
551
552impl Response {
553    /// Get schema for any JSON content type
554    ///
555    /// Prefers the canonical `application/json` entry, then falls back to
556    /// any `application/*+json` variant (RFC 6839) such as
557    /// `application/vnd.api+json`, `application/hal+json`, or
558    /// `application/problem+json`.
559    pub fn json_schema(&self) -> Option<&Schema> {
560        self.content
561            .as_ref()
562            .and_then(find_json_content)
563            .and_then(|(_, media_type)| media_type.schema.as_ref())
564    }
565}
566
567/// OpenAPI Media Type Object
568#[derive(Debug, Clone, Deserialize, Serialize)]
569pub struct MediaType {
570    pub schema: Option<Schema>,
571    #[serde(flatten)]
572    pub extra: BTreeMap<String, Value>,
573}
574
575#[cfg(test)]
576#[allow(clippy::unwrap_used, clippy::expect_used)]
577mod tests {
578    use super::*;
579    use serde_json::json;
580
581    #[test]
582    fn test_parse_simple_object_schema() {
583        let schema_json = json!({
584            "type": "object",
585            "properties": {
586                "name": {
587                    "type": "string",
588                    "description": "User name"
589                },
590                "age": {
591                    "type": "integer"
592                }
593            },
594            "required": ["name"]
595        });
596
597        let schema: Schema = serde_json::from_value(schema_json).unwrap();
598
599        match schema {
600            Schema::Typed {
601                schema_type: SchemaType::Object,
602                details,
603            } => {
604                assert!(details.properties.is_some());
605                assert_eq!(details.required, Some(vec!["name".to_string()]));
606                assert!(details.is_field_required("name"));
607                assert!(!details.is_field_required("age"));
608            }
609            _ => panic!("Expected object schema"),
610        }
611    }
612
613    #[test]
614    fn test_parse_string_enum() {
615        let schema_json = json!({
616            "type": "string",
617            "enum": ["active", "inactive", "pending"],
618            "description": "User status"
619        });
620
621        let schema: Schema = serde_json::from_value(schema_json).unwrap();
622
623        match schema {
624            Schema::Typed {
625                schema_type: SchemaType::String,
626                details,
627            } => {
628                assert!(details.is_string_enum());
629                let values = details.string_enum_values().unwrap();
630                assert_eq!(values, vec!["active", "inactive", "pending"]);
631            }
632            _ => panic!("Expected string enum schema"),
633        }
634    }
635
636    #[test]
637    fn test_parse_reference_schema() {
638        let schema_json = json!({
639            "$ref": "#/components/schemas/User"
640        });
641
642        let schema: Schema = serde_json::from_value(schema_json).unwrap();
643
644        assert!(schema.is_reference());
645        assert_eq!(schema.reference(), Some("#/components/schemas/User"));
646    }
647
648    #[test]
649    fn test_parse_discriminated_union() {
650        let schema_json = json!({
651            "oneOf": [
652                {"$ref": "#/components/schemas/Dog"},
653                {"$ref": "#/components/schemas/Cat"}
654            ],
655            "discriminator": {
656                "propertyName": "petType"
657            }
658        });
659
660        let schema: Schema = serde_json::from_value(schema_json).unwrap();
661
662        assert!(schema.is_discriminated_union());
663        let discriminator = schema.discriminator().unwrap();
664        assert_eq!(discriminator.property_name, "petType");
665    }
666
667    #[test]
668    fn test_parse_nullable_pattern() {
669        let schema_json = json!({
670            "anyOf": [
671                {"$ref": "#/components/schemas/User"},
672                {"type": "null"}
673            ]
674        });
675
676        let schema: Schema = serde_json::from_value(schema_json).unwrap();
677
678        assert!(schema.is_nullable_pattern());
679        let non_null = schema.non_null_variant().unwrap();
680        assert!(non_null.is_reference());
681    }
682
683    #[test]
684    fn is_json_media_type_accepts_canonical_and_structured_suffix() {
685        // Canonical
686        assert!(is_json_media_type("application/json"));
687        // Parameters tolerated (RFC 7231 ยง3.1.1.1)
688        assert!(is_json_media_type("application/json; charset=utf-8"));
689        assert!(is_json_media_type("APPLICATION/JSON"));
690        // RFC 6839 +json structured-syntax suffix
691        assert!(is_json_media_type("application/vnd.api+json"));
692        assert!(is_json_media_type("application/hal+json"));
693        assert!(is_json_media_type("application/problem+json"));
694        assert!(is_json_media_type("application/ld+json"));
695        assert!(is_json_media_type(
696            "application/vnd.api+json; charset=utf-8"
697        ));
698        // Negatives
699        assert!(!is_json_media_type("application/xml"));
700        assert!(!is_json_media_type("application/x-www-form-urlencoded"));
701        assert!(!is_json_media_type("text/plain"));
702        assert!(!is_json_media_type("application/jsonbutnotreally"));
703        // +json suffix only applies to application/* per RFC 6839
704        assert!(!is_json_media_type("text/something+json"));
705    }
706
707    #[test]
708    fn request_body_json_schema_finds_vnd_api_plus_json() {
709        // Mirrors Latitude.sh: request body declared under
710        // application/vnd.api+json without a sibling application/json.
711        let body_json = json!({
712            "required": true,
713            "content": {
714                "application/vnd.api+json": {
715                    "schema": {"$ref": "#/components/schemas/create_api_key"}
716                }
717            }
718        });
719
720        let body: RequestBody = serde_json::from_value(body_json).unwrap();
721        let schema = body.json_schema().expect("expected +json schema match");
722        assert!(schema.is_reference());
723    }
724
725    #[test]
726    fn request_body_best_content_prefers_canonical_json_over_plus_json() {
727        // When both are present (e.g. Latitude.sh's POST /auth/api_keys),
728        // best_content should still pick application/json for backwards
729        // compatibility with the existing snapshot suite.
730        let body_json = json!({
731            "required": true,
732            "content": {
733                "application/json": {
734                    "schema": {"$ref": "#/components/schemas/A"}
735                },
736                "application/vnd.api+json": {
737                    "schema": {"$ref": "#/components/schemas/B"}
738                }
739            }
740        });
741
742        let body: RequestBody = serde_json::from_value(body_json).unwrap();
743        let (ct, _) = body.best_content().expect("expected best_content");
744        assert_eq!(ct, "application/json");
745    }
746
747    #[test]
748    fn request_body_best_content_falls_back_to_plus_json() {
749        // When only the +json variant is declared, best_content returns
750        // it instead of skipping straight to form-urlencoded.
751        let body_json = json!({
752            "required": true,
753            "content": {
754                "application/vnd.api+json": {
755                    "schema": {"$ref": "#/components/schemas/B"}
756                }
757            }
758        });
759
760        let body: RequestBody = serde_json::from_value(body_json).unwrap();
761        let (ct, _) = body.best_content().expect("expected best_content");
762        assert_eq!(ct, "application/vnd.api+json");
763    }
764
765    #[test]
766    fn response_json_schema_finds_vnd_api_plus_json() {
767        // Mirrors every Latitude.sh response: schema lives under
768        // application/vnd.api+json only.
769        let resp_json = json!({
770            "description": "OK",
771            "content": {
772                "application/vnd.api+json": {
773                    "schema": {"$ref": "#/components/schemas/api_keys"}
774                }
775            }
776        });
777
778        let resp: Response = serde_json::from_value(resp_json).unwrap();
779        let schema = resp.json_schema().expect("expected +json schema match");
780        assert!(schema.is_reference());
781    }
782}