Swagger-ui: Recursive rendering needs work

Created on 2 Jul 2017  Ā·  34Comments  Ā·  Source: swagger-api/swagger-ui

Hello,

I have the following model definitions spec:

    "definitions": {
        "models.Equipment": {
            "title": "Equipment",
            "type": "object",
            "properties": {
                "Features": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/models.Feature"
                    }
                },
                "Id": {
                    "type": "integer",
                    "format": "int64"
                },
                "IdType": {
                    "type": "string"
                },
                "Name": {
                    "type": "string"
                },
                "Price": {
                    "type": "integer",
                    "format": "int32"
                }
            }
        },
        "models.Feature": {
            "title": "Feature",
            "type": "object",
            "properties": {
                "Equipments": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/models.Equipment"
                    }
                },
                "Id": {
                    "type": "integer",
                    "format": "int64"
                },
                "IdFeature": {
                    "$ref": "#/definitions/models.Feature"
                },
                "Name": {
                    "type": "string"
                }
            }
        }
    }

In the Feature model, the Equipments property is defined as an array of Equipment models, but Swagger UI 3.x renders it as an empty array []. Everywhere Feature model is used, like as examples for POST method in Feature I have this kind of display.

swagger-ui bug

We think it may be a bug caused by circular references. Thanks to Helen.

Thank you very much!

bug

Most helpful comment

Just ran into this as well using the OpenAPI 3 spec:

{
  "openapi": "3.0.0",
  "info": {
    "title": "Circular reference example",
    "version": "0.1"
  },
  "paths": {},
  "components": {
    "schemas": {
      "SelfReferencingSchema": {
        "type": "object",
        "properties": {
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SelfReferencingSchema"
            }
          }
        }
      }
    }
  }
}

Yields the following:
image

I'm not familiar with the history or previous implementations of this or what's possible/hard, but personally I think it'd be nice to show the title for a circular reference but not expand it by default. E.g.:
image

All 34 comments

@shockey @webron I mention you just to make sure this has been noticed, don't be offended :).

@julienkosinski thanks, I'll get this triaged on Monday šŸ˜„

I'm seeing two things here:

  1. Feature -> Equipments is being passed a $ref to render... this shouldn't be happening.
  2. The nested model is showing a very strange generated name:

image

This isn't quite right - we'll look into it šŸ˜„

In case it helps to find the issue => This indeed seems to be "Circular references" ... it also seems to appear without array wrapping.

"definitions": {
   "Classification": {
            "title": "Classification",
            "properties": {
                "ID": {
                    "type": "integer",
                    "default": 126132
                },
                "rank": {
                    "type": "string"
                },
                "scientificname": {
                    "type": "string"
                },
                "child": {
                    "$ref": "#/definitions/Classification"
                }
            },
            "xml": {
                "name": "Classification",
                "wrapped": false
            }
        }
}

Results in:
image

In the 2.x versions this was handled as follows:
image

Further the Model example values (JSON and XML) don't handle these nested definitions. The property is just removed from the list. But this could be another issue.

image

Just ran into this as well using the OpenAPI 3 spec:

{
  "openapi": "3.0.0",
  "info": {
    "title": "Circular reference example",
    "version": "0.1"
  },
  "paths": {},
  "components": {
    "schemas": {
      "SelfReferencingSchema": {
        "type": "object",
        "properties": {
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SelfReferencingSchema"
            }
          }
        }
      }
    }
  }
}

Yields the following:
image

I'm not familiar with the history or previous implementations of this or what's possible/hard, but personally I think it'd be nice to show the title for a circular reference but not expand it by default. E.g.:
image

Here's a screenshot of another example I was testing (screenshot was made using v3.3.1). The structure of the schema object for the ā€œcontainingFolderā€ attribute for ā€œFolderā€ is similar to the one for ā€œresourceForkā€ for ā€œFileā€; I’m guessing the reason that it’s not shown that ā€œcontainingFolderā€ is another ā€œFolderā€ is due to the circularity.

swagger_ui

Here’s the OpenAPI object:

{
  "openapi": "3.0.0",
  "info": {
    "title": "Test",
    "version": "0.1"
  },
  "components": {
    "schemas": {
      "Fork": {
        "type": "object",
        "properties": {
          "data": {
            "type": "string",
            "format": "base64"
          }
        }
      },
      "File": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "dataFork": {
            "$ref": "#/components/schemas/Fork"
          },
          "resourceFork": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Fork"
              }
            ]
          },
          "containingFolder": {
            "$ref": "#/components/schemas/Folder"
          }
        }
      },
      "Folder": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "containingFolder": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Folder"
              }
            ]
          },
          "containedItems": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/File"
                },
                {
                  "$ref": "#/components/schemas/Folder"
                }
              ]
            }
          }
        }
      }
    }
  }
}

@Rinzwind

          "resourceFork": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Fork"
              }
            ]

"type": "null" is not valid in OpenAPI, because there's no null type - that's one of the differences from JSON Schema. OpenAPI uses the nullable attribute instead.

I'm not sure if there's a way to combine $ref with nullable. You'll probably need to add nullable: true directly to the referenced schemas (Fork and Folder).

"type": "null" is not valid in OpenAPI, because there's no null type - that's one of the differences from JSON Schema. OpenAPI uses the nullable attribute instead.

@hkosova, I had missed that, thanks for pointing it out!

I'm not sure if there's a way to combine $ref with nullable. You'll probably need to add nullable: true directly to the referenced schemas (Fork and Folder).

I’m not sure that adding nullable directly to the referenced schemas is going to match with what I was trying to express: for the references to Fork from File, only the value for ā€œresourceForkā€ can be null, the value for ā€œdataForkā€ can not be null. I guess I could still express this as follows, which as far as I can tell is at least in accordance with the OpenAPI specification? But using an ā€œanyOfā€ with just one contained schema seems clumsy, is there a better way to handle this?

... 
"File": {
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "dataFork": {
      "$ref": "#/components/schemas/Fork"
    },
    "resourceFork": {
      "nullable": true,
      "anyOf": [
        {
          "$ref": "#/components/schemas/Fork"
        }
      ]
    },
    ...

@Rinzwind can't think of a different way to express it.

@Rinzwind can't think of a different way to express it.

Thanks for the feedback. I’ve posted an issue for it at the OpenAPI Specification repository.

Could you also use "allOf" . ? If so, would there be a difference between using the `anyOf`` method?

... 
"File": {
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "dataFork": {
      "$ref": "#/components/schemas/Fork"
    },
    "resourceFork": {
      "allOf": [
        {
          "$ref": "#/components/schemas/Fork"
        },
        {
          "nullable": true
        }
      ]
    },
    ...

allOf would actually be better than anyOf in this case.

@webron: I am not sure whether the example given by @jonschoning has the intended semantics.

The section on ā€œallOfā€ in the JSON Schema Validation proposal says: ā€œAn instance validates successfully against this keyword if it validates successfully against all schemas defined by this keyword's value.ā€

As far as I understand, null validates successfully against the schema { "nullable": true }. But null does not validate successfully against the schema { "$ref": "#/components/schemas/Fork"}. Therefore, I take it null also does not validate successfully against this schema:

{
  "allOf": [
    {
      "$ref": "#/components/schemas/Fork"
    },
    {
      "nullable": true
    } ] }

In other words, in @jonschoning's example, null would not be a valid value for ā€œresourceForkā€. Or did I misunderstand how schemas work in OpenAPI?

@rinzwind at the same time that allOf is interpreted the ref is dereferenced, so the net effect results in the nullable being a part of the refs definition. You can check this understanding by considering how allOf works with appending properties to a ref with allOf

Actually, @Rinzwind's point is correct, and that won't work. However, using anyOf also means that any type can be used because of nullable only schema.

If you find that an example is missing, feel free to open an issue on that repo. Not really sure what's not clear about it though.

@jonschoning As far as I understand, the following is meant by ā€œAllows sending a null value for the defined schema. Default value is false.ā€: ā€œWhen true, allows sending any value; when not present or false, allows sending any value except null.ā€

Rewording the definition according to the terminology employed in JSON Schema Validation (using in particular the wording for ā€œuniqueItemsā€ as inspiration):

nullable

The value of this keyword MUST be a boolean.

If this keyword has boolean value true, any instance validates successfully. If this keyword has boolean value false, any instance validates successfully unless it is null.

If not present, this keyword MUST be considered present with boolean value false.

I hope that helps. Keep in mind though that this is not a direct quote from the specification, but just my understanding of it.

Edit: I'm not quite sure whether my rewording in JSON Schema Validation terms is correct. It seems it would not imply that null validates successfully against {"type":"integer", "nullable": "true" } as intended. I'm not sure how to fix this. I'm inclined towards extending ā€œlinearityā€ with an additional exception that states that if the instance is null, and the keyword ā€œnullableā€ is present with value true, then the instance validates successfully regardless of other keywords. But I’m not sure at this point whether that is correct.

Ok, I understand how, thank you.

It seems one cannot simply specify a ref should be nullable then with anyOf or allOf without also changing what validates per Ron's comment

It seems one cannot simply specify a ref should be nullable then with anyOf or allOf without also changing what validates per Ron's comment

Indeed, as far as I understand, a schema like {"anyOf":[...,{"nullable":"true"}]} would allow null, but also any other value. The schema {"nullable":"true"} in OpenAPI is not equivalent to the schema {"type":"null"} in (pure) JSON Schema. I find the latter easier to understand, I'm not sure why OpenAPI introduced the former. I think I intuitively grasp its meaning, but as mentioned above, I'm having some trouble giving an exact definition for it. I agree with your earlier comment that the OpenAPI specification isn't very clear about the definition of ā€œnullableā€.

It looks like the original issue has been resolved, but there's still work to do on recursive rendering. The ticket has been renamed to reflect that.

It looks like the original issue has been resolved, but there's still work to do on recursive rendering. The ticket has been renamed to reflect that.

Hi, can I just check in to see if anyone has been working on this? I see the recursive rendering still not working in swagger

+1. I have the following object:

account:
  type: object
  allOf:
    - $ref: '../openapi.yaml#/components/schemas/baseEntity'
    - properties:    
        childAccounts:
          type: array
            items:
              $ref: '#/components/schemas/

This causes an error in Swagger-UI:

image

index.js:2247 TypeError: Cannot read property '1' of undefined
    at l (core.js:244)
    at Object.p [as applyPatch] (core.js:277)
    at Object.applyPatch (index.js:940)
    at e.value (index.js:2258)
    at index.js:2242
    at Array.forEach (<anonymous>)
    at e.value (index.js:2214)
    at index.js:2279
    at async Promise.all (:5001/index 0)
(anonymous) @ index.js:2247
value @ index.js:2214
(anonymous) @ index.js:2279
Promise.then (async)
value @ index.js:2276
(anonymous) @ index.js:2232
value @ index.js:2214
a @ index.js:2412
(anonymous) @ index.js:2390
value @ index.js:2380
(anonymous) @ index.js:2406
value @ index.js:2380
(anonymous) @ index.js:2406
value @ index.js:2380
(anonymous) @ index.js:2406
value @ index.js:2380
(anonymous) @ index.js:2406
value @ index.js:2380
(anonymous) @ index.js:2406
value @ index.js:2380
(anonymous) @ index.js:2406
value @ index.js:2380
v @ index.js:2419
Rt @ index.js:2707
(anonymous) @ index.js:2802
c @ runtime.js:45
(anonymous) @ runtime.js:271
e.<computed> @ runtime.js:97
o @ asyncToGenerator.js:5
s @ asyncToGenerator.js:27
(anonymous) @ asyncToGenerator.js:34
t @ _export.js:36
(anonymous) @ asyncToGenerator.js:23
Ft @ index.js:2813
In.resolveSubtree @ index.js:2777
resolveSubtree @ index.js:22
(anonymous) @ actions.js:178
c @ runtime.js:45
(anonymous) @ runtime.js:271
e.<computed> @ runtime.js:97
o @ asyncToGenerator.js:5
s @ asyncToGenerator.js:27
Promise.then (async)
o @ asyncToGenerator.js:15
s @ asyncToGenerator.js:27
(anonymous) @ asyncToGenerator.js:34
t @ _export.js:36
(anonymous) @ asyncToGenerator.js:23
(anonymous) @ actions.js:176
(anonymous) @ actions.js:176
c @ runtime.js:45
(anonymous) @ runtime.js:271
e.<computed> @ runtime.js:97
o @ asyncToGenerator.js:5
s @ asyncToGenerator.js:27
(anonymous) @ asyncToGenerator.js:34
t @ _export.js:36
(anonymous) @ asyncToGenerator.js:23
b @ debounce.js:95
x @ debounce.js:144
w @ debounce.js:132
setTimeout (async)
w @ debounce.js:135
setTimeout (async)
(anonymous) @ debounce.js:103
E @ debounce.js:172
(anonymous) @ actions.js:243
(anonymous) @ utils.js:134
(anonymous) @ bindActionCreators.js:3
(anonymous) @ OperationContainer.jsx:155
value @ OperationContainer.jsx:89
e.notifyAll @ CallbackQueue.js:74
close @ ReactReconcileTransaction.js:78
closeAll @ Transaction.js:207
perform @ Transaction.js:154
perform @ Transaction.js:141
perform @ ReactUpdates.js:87
w @ ReactUpdates.js:170
closeAll @ Transaction.js:207
perform @ Transaction.js:154
batchedUpdates @ ReactDefaultBatchingStrategy.js:60
e @ ReactUpdates.js:198
a @ ReactUpdateQueue.js:22
enqueueSetState @ ReactUpdateQueue.js:216
s.setState @ ReactBaseClasses.js:62
i.handleChange @ connect.js:302
f @ createStore.js:172
(anonymous) @ utils.js:137
(anonymous) @ bindActionCreators.js:3
(anonymous) @ wrap-actions.js:9
r @ system.js:174
(anonymous) @ system.js:461
(anonymous) @ index.js:22
r @ system.js:174
(anonymous) @ system.js:461
(anonymous) @ actions.js:77
(anonymous) @ utils.js:134
(anonymous) @ bindActionCreators.js:3
(anonymous) @ wrap-actions.js:5
r @ system.js:174
(anonymous) @ system.js:461
(anonymous) @ index.js:11
r @ system.js:174
(anonymous) @ system.js:461
p @ download-url.js:37
Promise.then (async)
(anonymous) @ download-url.js:26
(anonymous) @ utils.js:134
(anonymous) @ bindActionCreators.js:3
h @ index.js:153
Vn @ index.js:180
window.onload @ (index):42
load (async)
(anonymous) @ (index):39
Show 52 more frames
system.js:464 TypeError: Cannot read property 'every' of undefined
    at actions.js:191
    at reducers.js:93
    at immutable.js:3084
    at ft.__iterate (immutable.js:2206)
    at o.__iterateUncached (immutable.js:3083)
    at ce (immutable.js:604)
    at o.K.__iterate (immutable.js:320)
    at o.forEach (immutable.js:4381)
    at immutable.js:2069
    at ft.Ue.withMutations (immutable.js:1353)

As soon as I comment out that property, no errors in the UI.

+1. I use this definition:

"Comment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "post": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "parent": {
            "type": "string"
          },
          "author": {
            "$ref": "#/components/schemas/Author"
          },
          "createdAt": {
            "type": "string",
            "example": "2019-08-16T01:04:02.504Z"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Comment"
            }
          },
          "rating": {
            "type": "number"
          },
          "rated": {
            "$ref": "#/components/schemas/UserRate"
          }
        }
      },

children is null in response body.

Encounter the same problem of recursive data structure...swagger-ui just don't rendering it properly.
Is there any plan to fix this?

+1

+1

Is there any update on this?

+1 Seems to be related, but with different errors in UI (Swashbuckle.AspNetCore.SwaggerUI 5.0.0):
image

{
  "swagger": "2.0",
  "info": {
    "description": "Version 1",
    "version": "v1"
  },
  "paths": {
    "/Recursive": {
      "get": {
        "tags": [
          "Recursive"
        ],
        "produces": [
          "text/plain",
          "application/json",
          "text/json"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "schema": {
              "$ref": "#/definitions/RecursiveType"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "RecursiveType": {
      "type": "object",
      "properties": {
        "item": {
          "allOf": [
            {
              "$ref": "#/definitions/RecursiveType"
            }
          ],
          "readOnly": true
        }
      }
    }
  }
}

Hello,

I just encountered this issue as well. I have a list of Item instances. Each Item instance in that list has a property that's also a list of Item instances; a recursive data structure. Swagger does not like this.

Is there something that can be done for now, besides excluding the property that is?

Not the most elegant solution, but it is possible to show the user what type this property is.

  schemas:
    UserView:
      type: object
      properties:
        id:
        # ....
        # ....
        # ....
        master:
          $ref: '#/components/schemas/UserViewCircular'
      required:
        # ....
    UserViewCircular:
      $ref: '#/components/schemas/UserView'

Screenshot_1

Screenshot_2

There are also render errors in swagger hub swagger-editor.
I know that I've a recursive reference in my (quite complex) schema and it took me a long time to recognize that the UI error seems to come from there (see screenshot):
swagger-ui-render-error-recursive-references_2020-03-31

If I click randomly on some schema components in the left navigation bar, on nearly every click the editor increases the error count just saying the same error again and again: "error, line 0, undefined is not an object (evaluating 'm[b]')".

Hope there will be soon a fix for that available.
Thanks in advance,
Chris

I decided my problem with circular dependencies like this

components:
  schemas:
    CategoryWithReferences:
      allOf:
        - $ref: '#/components/schemas/Category'
        - type: object
          properties:
            parent:
              allOf:
                - $ref: '#/components/schemas/Category'
            childs:
              type: array
              items:
                allOf:
                  - $ref: '#/components/schemas/Category'

    Category:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
        name: 
          type: string

image

There's a very weird behavior with the call to SwaggerUIBundle. If the _url_ param is a absolute path, e.g. "/api/swagger.json", the JSON is loaded once (observed in devtools network monitor) and circular references appear empty. If the _url_ param is a relative path, e.g. "./swagger.json", the JSON is loaded twice and circular references are filled in!

Was this page helpful?
0 / 5 - 0 ratings