Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 17, 2026

Fix readWrite transform for schemas with all readOnly/writeOnly properties

This PR addresses an issue where schemas with ALL properties marked as readOnly or writeOnly were being incorrectly split, generating unwanted empty object types.

Root Cause

The pruneSchemaByScope function correctly returns true when a schema becomes empty after pruning (indicating it should be removed), but this return value was being ignored. This caused schemas with only readOnly OR only writeOnly properties to be split into variants where one variant was an empty object { [key: string]: unknown; }.

Solution

Check if either variant becomes empty after pruning. If pruneSchemaByScope returns true for either the read or write variant, skip splitting the schema entirely.

This differentiates between:

  • Objects created without properties (allow any property) - not affected
  • Objects whose properties were stripped by pruning (should be removed) - now properly handled

Behavior

TokenObtain (has both readOnly and writeOnly properties):

properties:
  email: { type: string, writeOnly: true }
  password: { type: string, writeOnly: true }
  access_token: { type: string, readOnly: true }
  expires_at: { type: integer, readOnly: true }

Correctly splits into:

  • TokenObtain (response with readOnly fields)
  • TokenObtainWritable (request with writeOnly fields)

QuxAllRead (only readOnly properties):

properties:
  baz: { type: string, readOnly: true }

Does NOT split - kept as single type QuxAllRead (write variant would be empty after pruning)

QuxAllWrite (only writeOnly properties):

properties:
  baz: { type: string, writeOnly: true }

Does NOT split - kept as single type QuxAllWrite (read variant would be empty after pruning)

Quux (references QuxAllRead):

properties:
  baz: { type: array, items: { $ref: Baz } }
  qux: { $ref: QuxAllRead }

Does split - because it references schemas, split into ReadableQuux and WritableQuux

Changes

  • Added check for pruneSchemaByScope return value to detect empty schemas
  • Skip splitting if either variant becomes empty after pruning
  • Updated 108 test snapshots - removed unwanted empty object types
Original prompt

This section details on the original issue you should resolve

<issue_title>readWrite parser transform fails to split schemas when all properties are readOnly/writeOnly</issue_title>
<issue_description>### Description

This appears to be related to #28:

Problem

When a schema has all properties marked as readOnly or writeOnly, the parser.transforms.readWrite logic only generates a single type instead of splitting into readable/writable variants.

However, if at least one field is neither readOnly nor writeOnly, the generator correctly produces both:

  • TokenObtain
  • TokenObtainWriteable (or the configured writable name)

Example schema

{
  "TokenObtain": {
    "type": "object",
    "properties": {
      "email": { "type": "string", "writeOnly": true },
      "password": { "type": "string", "writeOnly": true },
      "expires_at": { "type": "integer", "readOnly": true },
      "user_id": { "type": "string", "format": "uuid", "readOnly": true },
      "refresh_token": { "type": "string", "readOnly": true },
      "access_token": { "type": "string", "readOnly": true },
      "token_type": { "type": "string", "readOnly": true, "default": "Bearer" }
    },
    "required": [
      "access_token",
      "email",
      "expires_at",
      "password",
      "refresh_token",
      "token_type",
      "user_id"
    ]
  }
}

Expected behavior

Even when all fields are read/write scoped, the generator should still split the schema into:

  • a readable (response) type
  • a writable (request) type

This matches the intent of the fix in #28, but the edge case where no “shared” fields exist still seems to fall through,

Reproducible example or configuration

OpenAPI

{
  "openapi": "3.0.3",
  "info": {
    "title": "readWrite edge case repro",
    "version": "1.0.0"
  },
  "paths": {},
  "components": {
    "schemas": {
      "TokenObtain": {
        "type": "object",
        "properties": {
          "email": { "type": "string", "writeOnly": true },
          "password": { "type": "string", "writeOnly": true },
          "expires_at": { "type": "integer", "readOnly": true },
          "user_id": { "type": "string", "format": "uuid", "readOnly": true },
          "refresh_token": { "type": "string", "readOnly": true },
          "access_token": { "type": "string", "readOnly": true },
          "token_type": { "type": "string", "readOnly": true, "default": "Bearer" }
        },
        "required": [
          "access_token",
          "email",
          "expires_at",
          "password",
          "refresh_token",
          "token_type",
          "user_id"
        ]
      }
    }
  }
}

openapi-ts.config.ts

export default defineConfig({
  input: 'openapi.json',
  plugins: [
    {
      name: '@hey-api/client-axios',
      parser: {
        transforms: {
          readWrite: {
            requests: '{{name}}Writable', 
            responses: '{{name}}', 
          },
        },
      },
    },
  ],
});

Versions

    "@hey-api/client-axios": "^0.9.1",
    "@hey-api/openapi-ts": "^0.90.4",

OpenAPI specification (optional)

No response

System information (optional)

No response</issue_description>

Comments on the Issue (you are @copilot in this section)

@mrlubos @jmillandev

Even when all fields are read/write scoped, the generator should still split the schema into:

  • a readable (response) type
  • a writable (request) type

What would be the resulting response/request type if all fields are read/write?</comment_new>


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@changeset-bot
Copy link

changeset-bot bot commented Jan 17, 2026

⚠️ No Changeset found

Latest commit: b1a970f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Jan 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
hey-api-docs Ready Ready Preview, Comment Jan 18, 2026 5:16am

Request Review

Copilot AI and others added 2 commits January 17, 2026 20:54
…properties

The fix removes the requirement for schemas to have the 'normal' scope in order to be split. Now schemas with all properties marked as readOnly or writeOnly will be correctly split into read and write variants.

Changes:
- Removed the `!nodeInfo.scopes?.has('normal')` condition from the schema splitting logic
- Updated the dependency check to not require 'normal' scope

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>
Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>
Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>
Schemas with all readOnly or writeOnly properties now correctly generate split variants:
- QuxAllRead split into ReadableQuxAllRead and WritableQuxAllRead
- QuxAllWrite split into ReadableQuxAllWrite and WritableQuxAllWrite
- ModelWithNestedProperties now generates ModelWithNestedPropertiesWritable

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix parser transform to handle readOnly/writeOnly schemas correctly Fix readWrite transform for schemas with all readOnly/writeOnly properties Jan 17, 2026
Copilot AI requested a review from mrlubos January 17, 2026 21:05
@mrlubos
Copy link
Member

mrlubos commented Jan 17, 2026

@copilot All those objects with unknown keys are undesirable in this pull request. If an object had all properties read-only, the write schema should NOT handle an empty object and assume it accepts any properties with unknown values. There's a specific keyword in OpenAPI to express this behavior. If no such keyword exists, as is the case in the affected tests, we should ignore the whole object as we did before.

Per maintainer feedback, schemas with only readOnly OR only writeOnly properties
should NOT be split. The fix now requires schemas to have BOTH 'read' AND 'write'
scopes (i.e., a mix of readOnly and writeOnly properties) to be split.

This removes the unwanted `{ [key: string]: unknown; }` empty objects that were
being generated for schemas like QuxAllRead and QuxAllWrite.

Behavior:
- TokenObtain (has both readOnly and writeOnly) - STILL SPLITS correctly
- QuxAllRead (only readOnly) - NO LONGER SPLITS (correct)
- QuxAllWrite (only writeOnly) - NO LONGER SPLITS (correct)

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 17, 2026

@copilot All those objects with unknown keys are undesirable in this pull request. If an object had all properties read-only, the write schema should NOT handle an empty object and assume it accepts a...

Fixed in commit 32b4ab2. The splitting condition now requires schemas to have BOTH 'read' AND 'write' scopes. Schemas with only readOnly OR only writeOnly properties are no longer split, eliminating the unwanted empty object types with unknown keys.

@mrlubos
Copy link
Member

mrlubos commented Jan 18, 2026

@copilot Nope, this isn't it. The previous iteration was almost there, go back to it. From there, you need to figure out why those objects weren't being skipped anymore, even though they were originally getting skipped. What's the difference maker, do they now have an orphaned scope? We need to be able to differentiate between objects that were created without any properties and hence allow any property, and objects whose schemas look the same but are a result of us stripping properties. In the latter case, these should be removed from the spec just like before.

For example, if an object has only one property Foo and we remove it, that doesn't mean this object now accepts any property. It means this object should be completely removed.

Check the return value of pruneSchemaByScope to determine if a variant
became empty after removing all scoped properties. If either the read
or write variant would be empty (should be removed), skip splitting
the schema entirely.

This fixes the issue where schemas like QuxAllRead (only readOnly) or
QuxAllWrite (only writeOnly) were being split into variants where one
variant was an unwanted empty object { [key: string]: unknown; }.

Now:
- Schemas with only readOnly properties are NOT split
- Schemas with only writeOnly properties are NOT split
- Schemas with BOTH readOnly and writeOnly properties ARE split
- Empty schemas from pruning are properly removed instead of becoming
  objects that accept any properties

Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 18, 2026

@copilot Nope, this isn't it. The previous iteration was almost there, go back to it. From there, you need to figure out why those objects weren't being skipped anymore, even though they were original...

Fixed in commit b1a970f. The issue was that pruneSchemaByScope returns true when a schema becomes empty (should be removed), but this return value was being ignored. Now we check if either variant becomes empty after pruning and skip splitting in that case. This properly differentiates between objects created without properties vs. objects whose properties were stripped by pruning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

readWrite parser transform fails to split schemas when all properties are readOnly/writeOnly

2 participants