swift-openapi-generator icon indicating copy to clipboard operation
swift-openapi-generator copied to clipboard

Add readyOnly/writeOnly support

Open hactar opened this issue 4 months ago • 4 comments

Motivation

Currently the generator does not support readOnly and writeOnly, which is present in both 3.0 and 3.1. These attributes can be useful when generating RESTful APIs, for example if you have an ID that is generated on the server, so when you POST, there should not be a required ID field. but when you GET, the ID field is required in the response. The result would be that the same component would sometimes have a field that is optional, and sometimes not, depending on usage context.

Proposed solution

I guess adding this is not trivial, the generator would have to generate two different versions of components, one for the request, and one for the response? Or would there be a smarter way of doing this?

Alternatives considered

No response

Additional information

Is there a way you people currently work around this? Do you manually maintain two versions of the same component in the yaml file for example?

hactar avatar Aug 12 '25 14:08 hactar

Hi @hactar,

thanks for filing this. I agree this is an interesting question and I'm not sure I have a solution right now. Generating all schemas twice seems like a step too far, but I also want us to stay faithful to the spec.

Do you know what other typesafe language generators do?

czechboy0 avatar Aug 12 '25 19:08 czechboy0

Do you know what other typesafe language generators do?

While I was googling for information on readOnly and writeOnly, I stumbled across this discussion - a different framework with the same issue. Maybe that discussion is useful input. https://github.com/OpenAPITools/openapi-generator/issues/4190

Personally I have not worked with a framework that has this available yet, our back team and our client team are trying to clean up an API we use where as a workaround we currently send "0" - and other meaningless values such as creation dates that are ignored by the server during POST as the server is supposed to generate those - to the server during the POSTs, but we need those values to be required during GET. It's messy and while researching of how to solve this via openapi, readOnly and writeOnly were mentioned as the solution to that problem, which makes sense I'd say.

hactar avatar Aug 12 '25 19:08 hactar

A manual way, which I've generally used, is to define separate types for POSTs (creation) and GETs (fetching). For example, unique IDs are often assigned by the server, so it shouldn't need to be in the POST at all.

For example, check out this example of "*Request" types: https://github.com/swiftlang/swift-server-todos-tutorial/blob/0f7b57bd180ff3b3ca620fdda65141712d941040/Completed/Sources/SwiftServerTodos/openapi.yaml#L100

In a way, readOnly/writeOnly are just a first class way to represent this coupling of two related schemas.

czechboy0 avatar Aug 12 '25 20:08 czechboy0

Do you know what other typesafe language generators do?

We use readOnly and writeOnly in our OpenAPI files and a lot of generators / tools do not support this. But some do support this. I have here two examples:

Modified Pet Store

My modified pet store example :

...
components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
          readOnly: true
        name:
          type: string
        secret:
          type: string
          writeOnly: true
...

Generator Hey API

Hey API generates two types: Pet and PetWritable.

Generated TypeScript code:

...
export type Pet = {
    readonly id: number;
    name: string;
};

...

export type PetWritable = {
    name: string;
    secret?: string;
};
...

Generator typescript-fetch (OpenAPI Generator)

The generator typescript-fetch from the OpenAPI Generator uses a different approach. In short, there is a PetToWeb and PetFromWeb functions that handles the writeOnly and readOnly fields.

Generated code example:

export interface Pet {
    readonly id: number;
    name: string;
    secret?: string;
}

/**
 * Check if a given object implements the Pet interface.
 */
export function instanceOfPet(value: object): value is Pet {
    if (!('id' in value) || value['id'] === undefined) return false;
    if (!('name' in value) || value['name'] === undefined) return false;
    return true;
}

export function PetFromJSON(json: any): Pet {
    return PetFromJSONTyped(json, false);
}

export function PetFromJSONTyped(json: any, ignoreDiscriminator: boolean): Pet {
    if (json == null) {
        return json;
    }
    return {
        
        'id': json['id'],
        'name': json['name'],
        'secret': json['secret'] == null ? undefined : json['secret'],
    };
}

export function PetToJSON(json: any): Pet {
    return PetToJSONTyped(json, false);
}

export function PetToJSONTyped(value?: Omit<Pet, 'id'> | null, ignoreDiscriminator: boolean = false): any {
    if (value == null) {
        return value;
    }

    return {
        'name': value['name'],
        'secret': value['secret'],
    };
}

ManuW avatar Nov 09 '25 16:11 ManuW