1use std::collections::HashSet;
2
3use serde_json::Value;
4
5use crate::error::{RefError, ResolveError, StrictResolveError};
6use crate::resolved::ResolvedDoc;
7
8const MAX_DEPTH: u32 = 64;
10
11pub fn resolve(root: &Value) -> Result<ResolvedDoc, ResolveError> {
57 let mut ctx = Context {
58 root,
59 ref_errors: Vec::new(),
60 visiting: HashSet::new(),
61 seen_refs: HashSet::new(),
62 };
63 let value = resolve_value(&mut ctx, root, 0)?;
64 Ok(ResolvedDoc {
65 value,
66 ref_errors: ctx.ref_errors,
67 })
68}
69
70pub fn resolve_with_root<'a>(
95 root: &'a Value,
96 target: &'a Value,
97) -> Result<ResolvedDoc, ResolveError> {
98 let mut ctx = Context {
99 root,
100 ref_errors: Vec::new(),
101 visiting: HashSet::new(),
102 seen_refs: HashSet::new(),
103 };
104 let value = resolve_value(&mut ctx, target, 0)?;
105 Ok(ResolvedDoc {
106 value,
107 ref_errors: ctx.ref_errors,
108 })
109}
110
111pub fn resolve_strict(root: &Value) -> Result<Value, StrictResolveError> {
132 let doc = resolve(root)?;
133 Ok(doc.into_value()?)
134}
135
136struct Context<'a> {
141 root: &'a Value,
142 ref_errors: Vec<RefError>,
143 visiting: HashSet<String>,
144 seen_refs: HashSet<String>,
145}
146
147fn resolve_value(ctx: &mut Context<'_>, value: &Value, depth: u32) -> Result<Value, ResolveError> {
148 if depth > MAX_DEPTH {
149 return Err(ResolveError::DepthExceeded {
150 max_depth: MAX_DEPTH,
151 });
152 }
153 match value {
154 Value::Object(obj) => {
155 if let Some(ref_str) = obj.get("$ref").and_then(|v| v.as_str()) {
156 let resolved = resolve_ref(ctx, ref_str, depth)?;
157 if obj.len() > 1 {
160 if let Value::Object(mut resolved_obj) = resolved {
161 for (k, v) in obj {
162 if k != "$ref" {
163 resolved_obj.insert(k.clone(), resolve_value(ctx, v, depth + 1)?);
164 }
165 }
166 return Ok(Value::Object(resolved_obj));
167 }
168 push_ref_error(
170 ctx,
171 ref_str,
172 RefError::SiblingKeysIgnored {
173 ref_str: ref_str.to_string(),
174 },
175 );
176 }
177 return Ok(resolved);
178 }
179 let new_obj: serde_json::Map<String, Value> = obj
180 .iter()
181 .map(|(k, v)| resolve_value(ctx, v, depth + 1).map(|rv| (k.clone(), rv)))
182 .collect::<Result<_, _>>()?;
183 Ok(Value::Object(new_obj))
184 }
185 Value::Array(arr) => {
186 let new_arr: Vec<Value> = arr
187 .iter()
188 .map(|v| resolve_value(ctx, v, depth + 1))
189 .collect::<Result<_, _>>()?;
190 Ok(Value::Array(new_arr))
191 }
192 _ => Ok(value.clone()),
193 }
194}
195
196fn resolve_ref(ctx: &mut Context<'_>, ref_str: &str, depth: u32) -> Result<Value, ResolveError> {
197 let pointer = match ref_str.strip_prefix('#') {
199 Some(p) => p,
200 None => {
201 push_ref_error(
202 ctx,
203 ref_str,
204 RefError::External {
205 ref_str: ref_str.to_string(),
206 },
207 );
208 return Ok(serde_json::json!({ "$ref": ref_str }));
209 }
210 };
211
212 if ctx.visiting.contains(ref_str) {
214 push_ref_error(
215 ctx,
216 ref_str,
217 RefError::Cycle {
218 ref_str: ref_str.to_string(),
219 },
220 );
221 return Ok(serde_json::json!({ "$ref": ref_str }));
222 }
223
224 let root = ctx.root;
226 let target = match root.pointer(pointer) {
227 Some(v) => v,
228 None => {
229 push_ref_error(
230 ctx,
231 ref_str,
232 RefError::TargetNotFound {
233 ref_str: ref_str.to_string(),
234 },
235 );
236 return Ok(serde_json::json!({ "$ref": ref_str }));
237 }
238 };
239
240 ctx.visiting.insert(ref_str.to_string());
242 let resolved = resolve_value(ctx, target, depth + 1)?;
243 ctx.visiting.remove(ref_str);
244
245 Ok(resolved)
246}
247
248fn push_ref_error(ctx: &mut Context<'_>, ref_str: &str, error: RefError) {
249 if ctx.seen_refs.insert(ref_str.to_string()) {
250 ctx.ref_errors.push(error);
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use serde_json::{json, Value};
257
258 use crate::{
259 resolve, resolve_strict, resolve_with_root, RefError, ResolveError, StrictResolveError,
260 };
261
262 use super::MAX_DEPTH;
263
264 #[test]
269 fn resolve_simple_schema_ref() {
270 let spec = json!({
271 "components": {
272 "schemas": {
273 "User": { "type": "object", "properties": { "name": { "type": "string" } } }
274 }
275 },
276 "schema": { "$ref": "#/components/schemas/User" }
277 });
278 let doc = resolve(&spec).unwrap();
279
280 assert!(doc.is_complete());
281 assert_eq!(doc.value["schema"]["type"], "object");
282 assert_eq!(doc.value["schema"]["properties"]["name"]["type"], "string");
283 }
284
285 #[test]
286 fn resolve_parameter_ref() {
287 let spec = json!({
288 "components": {
289 "parameters": {
290 "LimitParam": { "name": "limit", "in": "query", "schema": { "type": "integer" } }
291 }
292 },
293 "params": [
294 { "$ref": "#/components/parameters/LimitParam" }
295 ]
296 });
297 let doc = resolve(&spec).unwrap();
298
299 assert!(doc.is_complete());
300 assert_eq!(doc.value["params"][0]["name"], "limit");
301 assert_eq!(doc.value["params"][0]["in"], "query");
302 }
303
304 #[test]
305 fn resolve_response_ref() {
306 let spec = json!({
307 "components": {
308 "responses": {
309 "NotFound": { "description": "Resource not found" }
310 }
311 },
312 "result": { "$ref": "#/components/responses/NotFound" }
313 });
314 let doc = resolve(&spec).unwrap();
315
316 assert!(doc.is_complete());
317 assert_eq!(doc.value["result"]["description"], "Resource not found");
318 }
319
320 #[test]
325 fn resolve_nested_refs() {
326 let spec = json!({
327 "components": {
328 "schemas": {
329 "Address": { "type": "object", "properties": { "city": { "type": "string" } } },
330 "User": {
331 "type": "object",
332 "properties": {
333 "address": { "$ref": "#/components/schemas/Address" }
334 }
335 }
336 }
337 },
338 "target": { "$ref": "#/components/schemas/User" }
339 });
340 let doc = resolve(&spec).unwrap();
341
342 assert!(doc.is_complete());
343 assert_eq!(
344 doc.value["target"]["properties"]["address"]["type"],
345 "object"
346 );
347 assert_eq!(
348 doc.value["target"]["properties"]["address"]["properties"]["city"]["type"],
349 "string"
350 );
351 }
352
353 #[test]
354 fn resolve_three_level_nesting() {
355 let spec = json!({
356 "components": {
357 "schemas": {
358 "Country": { "type": "string" },
359 "Address": {
360 "type": "object",
361 "properties": { "country": { "$ref": "#/components/schemas/Country" } }
362 },
363 "User": {
364 "type": "object",
365 "properties": { "address": { "$ref": "#/components/schemas/Address" } }
366 }
367 }
368 },
369 "root": { "$ref": "#/components/schemas/User" }
370 });
371 let doc = resolve(&spec).unwrap();
372
373 assert!(doc.is_complete());
374 assert_eq!(
375 doc.value["root"]["properties"]["address"]["properties"]["country"]["type"],
376 "string"
377 );
378 }
379
380 #[test]
385 fn resolve_all_of_with_refs() {
386 let spec = json!({
387 "components": {
388 "schemas": {
389 "Base": { "type": "object", "properties": { "id": { "type": "integer" } } },
390 "Extra": { "type": "object", "properties": { "tag": { "type": "string" } } }
391 }
392 },
393 "combined": {
394 "allOf": [
395 { "$ref": "#/components/schemas/Base" },
396 { "$ref": "#/components/schemas/Extra" }
397 ]
398 }
399 });
400 let doc = resolve(&spec).unwrap();
401
402 assert!(doc.is_complete());
403 let all_of = doc.value["combined"]["allOf"].as_array().unwrap();
404 assert_eq!(all_of[0]["properties"]["id"]["type"], "integer");
405 assert_eq!(all_of[1]["properties"]["tag"]["type"], "string");
406 }
407
408 #[test]
409 fn resolve_one_of_with_refs() {
410 let spec = json!({
411 "components": {
412 "schemas": {
413 "Cat": { "type": "object", "properties": { "purrs": { "type": "boolean" } } },
414 "Dog": { "type": "object", "properties": { "barks": { "type": "boolean" } } }
415 }
416 },
417 "pet": {
418 "oneOf": [
419 { "$ref": "#/components/schemas/Cat" },
420 { "$ref": "#/components/schemas/Dog" }
421 ]
422 }
423 });
424 let doc = resolve(&spec).unwrap();
425
426 assert!(doc.is_complete());
427 let one_of = doc.value["pet"]["oneOf"].as_array().unwrap();
428 assert_eq!(one_of[0]["properties"]["purrs"]["type"], "boolean");
429 assert_eq!(one_of[1]["properties"]["barks"]["type"], "boolean");
430 }
431
432 #[test]
437 fn resolve_with_root_basic() {
438 let root = json!({
439 "components": {
440 "schemas": {
441 "Item": { "type": "string" }
442 }
443 }
444 });
445 let target = json!({ "$ref": "#/components/schemas/Item" });
446 let doc = resolve_with_root(&root, &target).unwrap();
447
448 assert!(doc.is_complete());
449 assert_eq!(doc.value["type"], "string");
450 }
451
452 #[test]
457 fn no_refs_returns_identical() {
458 let spec = json!({
459 "type": "object",
460 "properties": { "x": { "type": "integer" } }
461 });
462 let doc = resolve(&spec).unwrap();
463
464 assert!(doc.is_complete());
465 assert_eq!(doc.value, spec);
466 }
467
468 #[test]
473 fn resolve_refs_in_array() {
474 let spec = json!({
475 "components": {
476 "schemas": {
477 "A": { "type": "string" },
478 "B": { "type": "integer" }
479 }
480 },
481 "items": [
482 { "$ref": "#/components/schemas/A" },
483 { "$ref": "#/components/schemas/B" }
484 ]
485 });
486 let doc = resolve(&spec).unwrap();
487
488 assert!(doc.is_complete());
489 assert_eq!(doc.value["items"][0]["type"], "string");
490 assert_eq!(doc.value["items"][1]["type"], "integer");
491 }
492
493 #[test]
498 fn detect_direct_cycle() {
499 let spec = json!({
500 "components": {
501 "schemas": {
502 "Node": {
503 "type": "object",
504 "properties": {
505 "child": { "$ref": "#/components/schemas/Node" }
506 }
507 }
508 }
509 },
510 "root": { "$ref": "#/components/schemas/Node" }
511 });
512 let doc = resolve(&spec).unwrap();
513
514 assert!(!doc.is_complete());
515 assert!(doc.ref_errors.contains(&RefError::Cycle {
516 ref_str: "#/components/schemas/Node".to_string(),
517 }));
518 assert_eq!(
519 doc.value["root"]["properties"]["child"]["$ref"],
520 "#/components/schemas/Node"
521 );
522 }
523
524 #[test]
525 fn detect_indirect_cycle() {
526 let spec = json!({
527 "components": {
528 "schemas": {
529 "A": { "type": "object", "properties": { "b": { "$ref": "#/components/schemas/B" } } },
530 "B": { "type": "object", "properties": { "a": { "$ref": "#/components/schemas/A" } } }
531 }
532 },
533 "start": { "$ref": "#/components/schemas/A" }
534 });
535 let doc = resolve(&spec).unwrap();
536
537 assert!(!doc.is_complete());
538 let has_cycle = doc
539 .ref_errors
540 .iter()
541 .any(|e| matches!(e, RefError::Cycle { .. }));
542 assert!(has_cycle);
543 }
544
545 #[test]
550 fn report_missing_ref() {
551 let spec = json!({
552 "schema": { "$ref": "#/components/schemas/DoesNotExist" }
553 });
554 let doc = resolve(&spec).unwrap();
555
556 assert_eq!(doc.ref_errors.len(), 1);
557 assert!(doc.ref_errors.contains(&RefError::TargetNotFound {
558 ref_str: "#/components/schemas/DoesNotExist".to_string(),
559 }));
560 assert_eq!(
561 doc.value["schema"]["$ref"],
562 "#/components/schemas/DoesNotExist"
563 );
564 }
565
566 #[test]
567 fn report_external_ref() {
568 let spec = json!({
569 "schema": { "$ref": "https://example.com/schemas/External.json" }
570 });
571 let doc = resolve(&spec).unwrap();
572
573 assert_eq!(doc.ref_errors.len(), 1);
574 assert!(doc.ref_errors.contains(&RefError::External {
575 ref_str: "https://example.com/schemas/External.json".to_string(),
576 }));
577 }
578
579 #[test]
580 fn multiple_unresolved_refs_deduplicated() {
581 let spec = json!({
582 "a": { "$ref": "#/components/schemas/Missing" },
583 "b": { "$ref": "#/components/schemas/Missing" },
584 "c": { "$ref": "#/components/schemas/AlsoMissing" }
585 });
586 let doc = resolve(&spec).unwrap();
587
588 assert_eq!(doc.ref_errors.len(), 2);
589 assert!(doc.ref_errors.contains(&RefError::TargetNotFound {
590 ref_str: "#/components/schemas/Missing".to_string(),
591 }));
592 assert!(doc.ref_errors.contains(&RefError::TargetNotFound {
593 ref_str: "#/components/schemas/AlsoMissing".to_string(),
594 }));
595 }
596
597 #[test]
602 fn resolve_null_value() {
603 let doc = resolve(&Value::Null).unwrap();
604 assert_eq!(doc.value, Value::Null);
605 assert!(doc.is_complete());
606 }
607
608 #[test]
609 fn resolve_scalar_value() {
610 let doc = resolve(&json!(42)).unwrap();
611 assert_eq!(doc.value, json!(42));
612 }
613
614 #[test]
615 fn resolve_empty_object() {
616 let doc = resolve(&json!({})).unwrap();
617 assert_eq!(doc.value, json!({}));
618 }
619
620 #[test]
621 fn resolve_empty_array() {
622 let doc = resolve(&json!([])).unwrap();
623 assert_eq!(doc.value, json!([]));
624 }
625
626 #[test]
631 fn ref_with_sibling_keys_merges() {
632 let spec = json!({
633 "components": {
634 "schemas": {
635 "Item": { "type": "string" }
636 }
637 },
638 "target": {
639 "$ref": "#/components/schemas/Item",
640 "description": "Overridden description"
641 }
642 });
643 let doc = resolve(&spec).unwrap();
644
645 assert!(doc.is_complete());
646 assert_eq!(doc.value["target"]["type"], "string");
647 assert_eq!(doc.value["target"]["description"], "Overridden description");
648 }
649
650 #[test]
651 fn ref_sibling_overrides_resolved_key() {
652 let spec = json!({
653 "components": {
654 "schemas": {
655 "Item": { "type": "string", "description": "Original" }
656 }
657 },
658 "target": {
659 "$ref": "#/components/schemas/Item",
660 "description": "Overridden"
661 }
662 });
663 let doc = resolve(&spec).unwrap();
664
665 assert!(doc.is_complete());
666 assert_eq!(doc.value["target"]["type"], "string");
667 assert_eq!(doc.value["target"]["description"], "Overridden");
668 }
669
670 #[test]
671 fn ref_sibling_with_nested_ref() {
672 let spec = json!({
673 "components": {
674 "schemas": {
675 "Name": { "type": "string" },
676 "User": { "type": "object", "properties": { "name": { "type": "string" } } }
677 }
678 },
679 "target": {
680 "$ref": "#/components/schemas/User",
681 "title": "Extended User",
682 "extra": { "$ref": "#/components/schemas/Name" }
683 }
684 });
685 let doc = resolve(&spec).unwrap();
686
687 assert!(doc.is_complete());
688 assert_eq!(doc.value["target"]["type"], "object");
689 assert_eq!(doc.value["target"]["title"], "Extended User");
690 assert_eq!(doc.value["target"]["extra"]["type"], "string");
691 }
692
693 #[test]
698 fn ref_sibling_keys_ignored_when_target_is_string() {
699 let spec = json!({
700 "definitions": { "status": "active" },
701 "target": {
702 "$ref": "#/definitions/status",
703 "description": "This will be dropped"
704 }
705 });
706 let doc = resolve(&spec).unwrap();
707
708 assert!(!doc.is_complete());
709 assert!(doc.ref_errors.contains(&RefError::SiblingKeysIgnored {
710 ref_str: "#/definitions/status".to_string(),
711 }));
712 assert_eq!(doc.value["target"], "active");
713 }
714
715 #[test]
716 fn ref_sibling_keys_ignored_when_target_is_array() {
717 let spec = json!({
718 "definitions": { "tags": ["a", "b"] },
719 "target": {
720 "$ref": "#/definitions/tags",
721 "description": "dropped"
722 }
723 });
724 let doc = resolve(&spec).unwrap();
725
726 assert!(!doc.is_complete());
727 assert!(doc.ref_errors.contains(&RefError::SiblingKeysIgnored {
728 ref_str: "#/definitions/tags".to_string(),
729 }));
730 assert_eq!(doc.value["target"], json!(["a", "b"]));
731 }
732
733 #[test]
734 fn ref_sibling_keys_ignored_not_reported_for_object_target() {
735 let spec = json!({
736 "definitions": { "item": { "type": "string" } },
737 "target": {
738 "$ref": "#/definitions/item",
739 "description": "merged"
740 }
741 });
742 let doc = resolve(&spec).unwrap();
743
744 assert!(doc.is_complete());
745 assert_eq!(doc.value["target"]["type"], "string");
746 assert_eq!(doc.value["target"]["description"], "merged");
747 }
748
749 #[test]
750 fn ref_sibling_keys_ignored_strict_mode_fails() {
751 let spec = json!({
752 "definitions": { "val": 42 },
753 "target": {
754 "$ref": "#/definitions/val",
755 "title": "dropped"
756 }
757 });
758 let err = resolve_strict(&spec).unwrap_err();
759
760 assert!(matches!(err, StrictResolveError::Partial(_)));
761 assert_eq!(err.ref_errors().len(), 1);
762 assert_eq!(err.ref_errors()[0].ref_str(), "#/definitions/val");
763 }
764
765 #[test]
770 fn resolve_ref_with_slash_in_key() {
771 let spec = json!({
772 "definitions": {
773 "application/json": { "type": "string" }
774 },
775 "target": { "$ref": "#/definitions/application~1json" }
776 });
777 let doc = resolve(&spec).unwrap();
778
779 assert!(doc.is_complete());
780 assert_eq!(doc.value["target"]["type"], "string");
781 }
782
783 #[test]
784 fn resolve_ref_with_tilde_in_key() {
785 let spec = json!({
786 "definitions": {
787 "my~schema": { "type": "integer" }
788 },
789 "target": { "$ref": "#/definitions/my~0schema" }
790 });
791 let doc = resolve(&spec).unwrap();
792
793 assert!(doc.is_complete());
794 assert_eq!(doc.value["target"]["type"], "integer");
795 }
796
797 #[test]
802 fn ref_non_string_value_treated_as_regular_object() {
803 let spec = json!({
804 "schema": { "$ref": 123 }
805 });
806 let doc = resolve(&spec).unwrap();
807
808 assert!(doc.is_complete());
809 assert_eq!(doc.value["schema"]["$ref"], 123);
810 }
811
812 #[test]
817 fn root_self_reference_detected_as_cycle() {
818 let spec = json!({
819 "type": "object",
820 "self": { "$ref": "#" }
821 });
822 let doc = resolve(&spec).unwrap();
823
824 assert!(!doc.is_complete());
825 assert!(doc.ref_errors.contains(&RefError::Cycle {
826 ref_str: "#".to_string(),
827 }));
828 }
829
830 #[test]
835 fn depth_exceeded_is_fatal() {
836 let mut value = json!({ "leaf": true });
837 for _ in 0..=MAX_DEPTH {
838 value = json!({ "nested": value });
839 }
840 let result = resolve(&value);
841 assert!(matches!(
842 result,
843 Err(ResolveError::DepthExceeded {
844 max_depth: MAX_DEPTH
845 })
846 ));
847 }
848
849 #[test]
854 fn resolve_with_root_missing_ref() {
855 let root = json!({});
856 let target = json!({ "$ref": "#/components/schemas/Missing" });
857 let doc = resolve_with_root(&root, &target).unwrap();
858
859 assert!(doc.ref_errors.contains(&RefError::TargetNotFound {
860 ref_str: "#/components/schemas/Missing".to_string(),
861 }));
862 }
863
864 #[test]
869 fn resolve_strict_ok() {
870 let spec = json!({
871 "components": { "schemas": { "Id": { "type": "integer" } } },
872 "field": { "$ref": "#/components/schemas/Id" }
873 });
874 let value = resolve_strict(&spec).unwrap();
875 assert_eq!(value["field"]["type"], "integer");
876 }
877
878 #[test]
879 fn resolve_strict_fails_on_unresolved_ref() {
880 let spec = json!({
881 "schema": { "$ref": "#/missing" }
882 });
883 let err = resolve_strict(&spec).unwrap_err();
884 assert!(matches!(err, StrictResolveError::Partial(_)));
885 }
886
887 #[test]
888 fn resolve_strict_fails_on_fatal_error() {
889 let mut value = json!({ "leaf": true });
890 for _ in 0..=MAX_DEPTH {
891 value = json!({ "nested": value });
892 }
893 let err = resolve_strict(&value).unwrap_err();
894 assert!(matches!(
895 err,
896 StrictResolveError::Fatal(ResolveError::DepthExceeded { .. })
897 ));
898 }
899
900 #[test]
905 fn strict_error_partial_value_accessor() {
906 let spec = json!({
907 "components": { "schemas": { "Id": { "type": "integer" } } },
908 "ok": { "$ref": "#/components/schemas/Id" },
909 "broken": { "$ref": "#/missing" }
910 });
911 let err = resolve_strict(&spec).unwrap_err();
912
913 let value = err.partial_value().unwrap();
914 assert_eq!(value["ok"]["type"], "integer");
915 assert_eq!(value["broken"]["$ref"], "#/missing");
916
917 assert_eq!(err.ref_errors().len(), 1);
918 assert_eq!(err.ref_errors()[0].ref_str(), "#/missing");
919 }
920
921 #[test]
926 fn resolve_realistic_openapi_spec() {
927 let spec = json!({
928 "openapi": "3.0.3",
929 "components": {
930 "schemas": {
931 "Error": {
932 "type": "object",
933 "properties": {
934 "code": { "type": "integer" },
935 "message": { "type": "string" }
936 },
937 "required": ["code", "message"]
938 },
939 "User": {
940 "type": "object",
941 "properties": {
942 "id": { "type": "string" },
943 "email": { "type": "string", "format": "email" }
944 }
945 }
946 },
947 "parameters": {
948 "UserId": {
949 "name": "user_id",
950 "in": "path",
951 "required": true,
952 "schema": { "type": "string" }
953 }
954 }
955 },
956 "paths": {
957 "/users/{user_id}": {
958 "get": {
959 "parameters": [
960 { "$ref": "#/components/parameters/UserId" }
961 ],
962 "responses": {
963 "200": {
964 "content": {
965 "application/json": {
966 "schema": { "$ref": "#/components/schemas/User" }
967 }
968 }
969 },
970 "404": {
971 "content": {
972 "application/json": {
973 "schema": { "$ref": "#/components/schemas/Error" }
974 }
975 }
976 }
977 }
978 }
979 }
980 }
981 });
982 let doc = resolve(&spec).unwrap();
983
984 assert!(doc.is_complete(), "ref_errors: {:?}", doc.ref_errors);
985
986 let param = &doc.value["paths"]["/users/{user_id}"]["get"]["parameters"][0];
987 assert_eq!(param["name"], "user_id");
988 assert_eq!(param["in"], "path");
989
990 let user_schema = &doc.value["paths"]["/users/{user_id}"]["get"]["responses"]["200"]
991 ["content"]["application/json"]["schema"];
992 assert_eq!(user_schema["type"], "object");
993 assert_eq!(user_schema["properties"]["email"]["format"], "email");
994
995 let error_schema = &doc.value["paths"]["/users/{user_id}"]["get"]["responses"]["404"]
996 ["content"]["application/json"]["schema"];
997 assert_eq!(error_schema["properties"]["code"]["type"], "integer");
998 }
999}