Repository: openapi-ts/openapi-typescript (TypeScript)

Issue:
Unexpected error when property schemas are boolean
### openapi-typescript version

7.6.1

### Node.js version

20.5.1

### OS + version

macOS 15.3.1

### Description

If an object property is defined with a boolean schema transformation will fail. There is precedent in the codebase to handle these as `unknown` and `never` respectively (see `additionalProperties`).

### Reproduction

See the test in the linked PR.

### Expected result

See the test in the linked PR.

### Required

- [x] My OpenAPI schema is valid and passes the [Redocly validator](https://redocly.com/docs/cli/commands/lint/) (`npx @redocly/cli@latest lint`)

### Extra

- [x] I’m willing to open a PR (see [CONTRIBUTING.md](https://github.com/openapi-ts/openapi-typescript/blob/main/packages/openapi-typescript/CONTRIBUTING.md))


Discussion:
Thanks for the issue and PR. We’d be willing to accept it given there’s a precedent here. Is there a link to the specification where this is valid, either according to any OpenAPI 3.x version, or JSONSchema? This is the first time I’ve seen this syntax before
Sure! A boolean is always a valid JSON schema in versions later than draft 4 regardless of keyword. See the type keyword in either of these meta schemas:
https://json-schema.org/draft/2020-12/schema
https://json-schema.org/draft/2019-09/schema
https://json-schema.org/draft-07/schema
https://json-schema.org/draft-06/schema

Draft 4 (which OpenAPI <=3.0 was based on) only accepts them for specific keywords:
https://json-schema.org/draft-04/schema

It's my understanding that OpenAPI >=3.1 supports draft 2020 though.

Thanks!
Thanks for the reference! That works for me. This library doesn’t require a specific JSONSchema version; as long as a JSONSchema feature doesn’t conflict with any existing behavior it’s a simple add



Patch:
diff --git a/.changeset/clever-worms-smile.md b/.changeset/clever-worms-smile.md
new file mode 100644
index 000000000..e4fe743c6
--- /dev/null
+++ b/.changeset/clever-worms-smile.md
@@ -0,0 +1,5 @@
+---
+"openapi-typescript": patch
+---
+
+Fix boolean object property schemas
diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts
index a41ce0e69..baa143933 100644
--- a/packages/openapi-typescript/src/transform/schema-object.ts
+++ b/packages/openapi-typescript/src/transform/schema-object.ts
@@ -453,38 +453,46 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor
     // properties
     if (Object.keys(schemaObject.properties ?? {}).length) {
       for (const [k, v] of getEntries(schemaObject.properties ?? {}, options.ctx)) {
-        if (typeof v !== "object" || Array.isArray(v)) {
+        if ((typeof v !== "object" && typeof v !== "boolean") || Array.isArray(v)) {
           throw new Error(
-            `${options.path}: invalid property ${k}. Expected Schema Object, got ${
+            `${options.path}: invalid property ${k}. Expected Schema Object or boolean, got ${
               Array.isArray(v) ? "Array" : typeof v
             }`,
           );
         }
 
+        const { $ref, readOnly, hasDefault } =
+          typeof v === "object"
+            ? {
+                $ref: "$ref" in v && v.$ref,
+                readOnly: "readOnly" in v && v.readOnly,
+                hasDefault: "default" in v && v.default !== undefined,
+              }
+            : {};
+
         // handle excludeDeprecated option
         if (options.ctx.excludeDeprecated) {
-          const resolved = "$ref" in v ? options.ctx.resolve<SchemaObject>(v.$ref) : v;
-          if (resolved?.deprecated) {
+          const resolved = $ref ? options.ctx.resolve<SchemaObject>($ref) : v;
+          if ((resolved as SchemaObject)?.deprecated) {
             continue;
           }
         }
         let optional =
           schemaObject.required?.includes(k) ||
           (schemaObject.required === undefined && options.ctx.propertiesRequiredByDefault) ||
-          ("default" in v &&
+          (hasDefault &&
             options.ctx.defaultNonNullable &&
             !options.path?.includes("parameters") &&
             !options.path?.includes("requestBody") &&
             !options.path?.includes("requestBodies")) // can’t be required, even with defaults
             ? undefined
             : QUESTION_TOKEN;
-        let type =
-          "$ref" in v
-            ? oapiRef(v.$ref)
-            : transformSchemaObject(v, {
-                ...options,
-                path: createRef([options.path, k]),
-              });
+        let type = $ref
+          ? oapiRef($ref)
+          : transformSchemaObject(v, {
+              ...options,
+              path: createRef([options.path, k]),
+            });
 
         if (typeof options.ctx.transform === "function") {
           const result = options.ctx.transform(v as SchemaObject, options);
@@ -500,7 +508,7 @@ function transformSchemaObjectCore(schemaObject: SchemaObject, options: Transfor
 
         const property = ts.factory.createPropertySignature(
           /* modifiers     */ tsModifiers({
-            readonly: options.ctx.immutable || ("readOnly" in v && !!v.readOnly),
+            readonly: options.ctx.immutable || readOnly,
           }),
           /* name          */ tsPropertyIndex(k),
           /* questionToken */ optional,


Test patch:
diff --git a/packages/openapi-typescript/test/transform/schema-object/object.test.ts b/packages/openapi-typescript/test/transform/schema-object/object.test.ts
index d9b7d7175..062f734a5 100644
--- a/packages/openapi-typescript/test/transform/schema-object/object.test.ts
+++ b/packages/openapi-typescript/test/transform/schema-object/object.test.ts
@@ -28,6 +28,23 @@ describe("transformSchemaObject > object", () => {
         // options: DEFAULT_OPTIONS,
       },
     ],
+    [
+      "property > boolean",
+      {
+        given: {
+          type: "object",
+          required: ["truthy", "falsy"],
+          properties: {
+            truthy: true,
+            falsy: false,
+          },
+        },
+        want: `{
+    truthy: unknown;
+    falsy: never;
+}`,
+      },
+    ],
     [
       "empty",
       {