Repository: enzonotario/vitepress-openapi (ts)

Issue:
"nullable" objects
### Current behavior

In OpenAPI you can create a nullable object with:
```yaml
type: [object, 'null']
properties:
  example:
    type: string
```

Currently, vitepress-openapi will not display the properties correctly.

### Desired behavior

If the object is nullable, it should still list all properties of nullable object and not just `object | null`

### Reproduction

_No response_

### Steps to reproduce

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Nullable objects"
  },
  "servers": [
    {
      "url": "https://whatever.com",
      "description": "Mock Server"
    }
  ],
  "paths": {
    "/nullable": {
      "get": {
        "tags": [
          "Nullable"
        ],
        "summary": "Get nullable object",
        "description": "Yayy",
        "operationId": "getNullableObject",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "a": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "a": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "tags": [],
  "externalDocs": {}
}
```

![Image](https://github.com/user-attachments/assets/1443d6bc-df78-4ab7-8f54-9fb784658230)

### Logs and Error Messages

_No response_

### Other Information

_No response_

Pull request description:
feat(Schemas): support nullable object types
<!-- Provide a general summary of your changes in the title above. -->

# Description

Adds support for schemas with `type: ["object", "null"]`

## Related issues/external references

Closes #206 

## Types of changes

- New feature


Patch:
diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md
index 5208de6..3dcdd7c 100644
--- a/docs/guide/getting-started.md
+++ b/docs/guide/getting-started.md
@@ -22,9 +22,21 @@ Before you begin, make sure you have the following installed:
 - VitePress
 - OpenAPI Specification (Version 3)
 
-## Installation
+## Starter Template
+
+To get started quickly with `vitepress-openapi`, you can use the provided starter template. This template includes a basic VitePress setup with `vitepress-openapi` and a sample OpenAPI specification.
+
+You can access the template from:
+- GitHub: [https://github.com/enzonotario/vitepress-openapi-starter](https://github.com/enzonotario/vitepress-openapi-starter)
+- Stackblitz: [https://stackblitz.com/fork/github/enzonotario/vitepress-openapi-starter](https://stackblitz.com/fork/github/enzonotario/vitepress-openapi-starter)
+
+You can also use the template to create a new VitePress project using the following command:
 
-Install the `vitepress-openapi` package using your package manager.
+```sh
+npx degit enzonotario/vitepress-openapi-starter my-api-docs
+```
+
+## Installation
 
 ::: code-group
 
diff --git a/docs/public/openapi-schemas.json b/docs/public/openapi-schemas.json
index 4a16d0c..446606c 100644
--- a/docs/public/openapi-schemas.json
+++ b/docs/public/openapi-schemas.json
@@ -102,6 +102,25 @@
           }
         }
       }
+    },
+    "/nullable-object": {
+      "get": {
+        "summary": "Get a nullable object",
+        "operationId": "getNullableObject",
+        "description": "Example of a JSON object that can be null.",
+        "responses": {
+          "200": {
+            "description": "A nullable object",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/NullableObject"
+                }
+              }
+            }
+          }
+        }
+      }
     }
   },
   "components": {
@@ -347,6 +366,20 @@
             "type": "string"
           }
         }
+      },
+      "NullableObject": {
+        "type": ["object", "null"],
+        "properties": {
+          "id": {
+            "type": "string"
+          },
+          "name": {
+            "type": "string"
+          },
+          "description": {
+            "type": "string"
+          }
+        }
       }
     }
   }
diff --git a/src/lib/examples/getSchemaUiJson.ts b/src/lib/examples/getSchemaUiJson.ts
index 868ec90..887b1b5 100644
--- a/src/lib/examples/getSchemaUiJson.ts
+++ b/src/lib/examples/getSchemaUiJson.ts
@@ -1,4 +1,4 @@
-import type { OAProperty } from '../parser/getSchemaUi'
+import type { JSONSchemaType, OAProperty } from '../parser/getSchemaUi'
 import { literalTypes } from '../../index'
 import { getPropertyExample } from './getPropertyExample'
 
@@ -38,7 +38,7 @@ function uiPropertyToJson(property: OAProperty, useExample: boolean): any {
     }, useExample)
   }
 
-  if (isSingleType(property, 'object')) {
+  if (containsType(property, 'object')) {
     if (isOneOfProperty(property)) {
       return resolveOneOfProperty(property, useExample)
     }
@@ -141,6 +141,10 @@ function hasAllLiteralTypes(property: OAProperty): boolean {
   return property.types?.every(type => type && literalTypes.includes(type))
 }
 
+function containsType(property: OAProperty, type: JSONSchemaType): boolean {
+  return property.types?.includes(type) ?? false
+}
+
 function isOneOfProperty(property: OAProperty): boolean {
   return !!property.meta?.isOneOf && Array.isArray(property.properties)
 }
diff --git a/src/lib/parser/getSchemaUi.ts b/src/lib/parser/getSchemaUi.ts
index 823e0b0..9b4ad94 100644
--- a/src/lib/parser/getSchemaUi.ts
+++ b/src/lib/parser/getSchemaUi.ts
@@ -4,7 +4,7 @@ import { getPropertyExamples } from '../examples/getPropertyExamples'
 import { getConstraints, hasConstraints } from './constraintsParser'
 import { resolveCircularRef } from './resolveCircularRef'
 
-type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null'
+export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null'
 
 interface Metadata {
   isCircularReference?: boolean
@@ -149,63 +149,57 @@ class UiPropertyFactory {
 
     const property = UiPropertyFactory.createBaseProperty(name, schema, required)
 
-    switch (schema.type) {
-      case 'array':
-        if (schema.items) {
-          const schemaType = determineSchemaType(schema.items)
-
-          property.properties = schemaType === 'object'
-            ? UiPropertyFactory.extractProperties(
-                schema.items.properties,
-                schema.items.required || [],
-                schema.items.additionalProperties,
-              )
-            : undefined
-
-          if (schemaType !== undefined) {
-            property.subtype = schemaType as JSONSchemaType
-          }
-
-          const itemsExamples = getPropertyExamples(schema.items)
-          if (itemsExamples) {
-            property.subexamples = itemsExamples
-          }
-
-          if (schema.items.const !== undefined) {
-            property.meta = { ...(property.meta || {}), isConstant: true }
-          }
-
-          if (schema.items.oneOf) {
-            property.meta = { ...(property.meta || {}), isOneOf: true }
-            property.properties = schema.items.oneOf.map((prop: any) => {
-              const propSchema = { ...prop, type: schema.items.type }
-              return {
-                ...UiPropertyFactory.schemaToUiProperty('', propSchema),
-                meta: { ...(prop.meta || {}), isOneOfItem: true },
-              }
-            })
-          }
+    if (Array.isArray(schema.type) ? schema.type.includes('array') : schema.type === 'array') {
+      if (schema.items) {
+        const schemaType = determineSchemaType(schema.items)
+
+        property.properties = schemaType === 'object'
+          ? UiPropertyFactory.extractProperties(
+              schema.items.properties,
+              schema.items.required || [],
+              schema.items.additionalProperties,
+            )
+          : undefined
+
+        if (schemaType !== undefined) {
+          property.subtype = schemaType as JSONSchemaType
+        }
+
+        const itemsExamples = getPropertyExamples(schema.items)
+        if (itemsExamples) {
+          property.subexamples = itemsExamples
+        }
+
+        if (schema.items.const !== undefined) {
+          property.meta = { ...(property.meta || {}), isConstant: true }
         }
-        break
 
-      case 'object':
+        if (schema.items.oneOf) {
+          property.meta = { ...(property.meta || {}), isOneOf: true }
+          property.properties = schema.items.oneOf.map((prop: any) => {
+            const propSchema = { ...prop, type: schema.items.type }
+            return {
+              ...UiPropertyFactory.schemaToUiProperty('', propSchema),
+              meta: { ...(prop.meta || {}), isOneOfItem: true },
+            }
+          })
+        }
+      }
+    } else if (Array.isArray(schema.type) ? schema.type.includes('object') : schema.type === 'object') {
+      property.properties = UiPropertyFactory.extractProperties(
+        schema.properties,
+        schema.required || [],
+        schema.additionalProperties,
+      )
+    } else if (schema.type === undefined) {
+      if (schema.properties || schema.additionalProperties) {
+        property.types = ['object']
         property.properties = UiPropertyFactory.extractProperties(
           schema.properties,
           schema.required || [],
           schema.additionalProperties,
         )
-        break
-
-      case undefined:
-        if (schema.properties || schema.additionalProperties) {
-          property.types = ['object']
-          property.properties = UiPropertyFactory.extractProperties(
-            schema.properties,
-            schema.required || [],
-            schema.additionalProperties,
-          )
-        }
-        break
+      }
     }
 
     return property


Test patch:
diff --git a/test/lib/processOpenAPI/getSchemaUi.test.ts b/test/lib/processOpenAPI/getSchemaUi.test.ts
index e602c19..532b65b 100644
--- a/test/lib/processOpenAPI/getSchemaUi.test.ts
+++ b/test/lib/processOpenAPI/getSchemaUi.test.ts
@@ -1316,6 +1316,29 @@ const fixtures: Record<string, FixtureTest> = {
       age: 42,
     },
   },
+
+  'object nullable': {
+    jsonSchema: {
+      type: ['object', 'null'],
+      properties: {
+        name: { type: 'string' },
+        age: { type: 'integer' },
+      },
+    },
+    schemaUi: {
+      name: '',
+      properties: [
+        { name: 'name', required: false, types: ['string'] },
+        { name: 'age', required: false, types: ['integer'] },
+      ],
+      types: ['object', 'null'],
+      required: false,
+    },
+    schemaUiJson: {
+      name: 'string',
+      age: 0,
+    },
+  },
 }
 
 describe('getSchemaUi and getSchemaUiJson from fixtures', () => {