Apicurio / apicurio-studio

Open Source API Design
https://www.apicur.io/studio/
Apache License 2.0
982 stars 494 forks source link

Request body properties can't be defined #644

Open anatoli26 opened 5 years ago

anatoli26 commented 5 years ago

I'm not sure this is something that the OpenAPI mandates to be implemented the way it is now, but currently there is no way to define a set of properties for a Request body, it's only possible to select a single data type - either a basic one (int, string) or one of the data types defined by the user.

Also, one can't define multiple request bodies with the same media type (e.g. application/json).

I have an example at hand right now: a user data type that has firstname, lastname, email, enabled, admin and other properties as part of the data type, but not password. The PATCH method could be used to change any of the properties if the user is admin, and only firstname, lastname and email if the user is a regular user. This method could also be used to change the password, but in this case either a prt (password reset token) should be provided or the current password and a new password (password_new) if the user is changing his own password. Or, if the user is admin and changing other user's password, just password_new is enough.

Today I have to either create multiple user data types for all this combinations or to embed all the possible fields with the explanation of each combination and condition to a single user data type, or (as I do it now) to specify everything in the description of the method and provide the possible combinations in the examples of the single request body media type 'application/json'.

I believe it would be better to be able to define multiple request body variants with the same media type and custom properties sets, maybe with a Name and a Description for each one (basically, for the request bodies to be definable the same way the user data types are).

GimpMaster commented 5 years ago

Bumping up this issue. We pretty much use the request body as our POST method for passing parameters. We use: "application/json" as the content type. We then define the parameters giving some as required and non-required as well as examples. This renders perfectly in ReDoc but Apicurio provides no way to edit this other than in the source code. Here is a working endpoint example that renders great in ReDoc, it even imports in Apicurio but there is no method to edit it in a GUI fashion.

post:
    tags:
        - Order
    summary: Order Failed
    description: >
        Call this endpoint on order failure. The fields below are for your convenience for updating any
        particular value that has changed. The important parameter is the status value.
    operationId: order_failed
    requestBody:
        content:
            application/json:
                schema:
                    required:
                        - status
                        - source_order_id
                    properties:
                        id:
                            description: (Optional) If provided it will update a previous order's information.
                            type: integer
                            example: 12345
                        first_name:
                            description: The customer's first name.
                            type: string
                            example: John
                        last_name:
                            description: The customer's last name.
                            type: string
                            example: Doe
                        email:
                            description: The customer's email.
                            type: string
                            example: john@doe.com
                        phone:
                            description: The customer's phone number.
                            type: string
                            example: +1-555-555-4444
                        currency:
                            description: (Optional) Provide a different currency then the merchant's default currency.
                            type: string
                            example: CDN
                        source_order_id:
                            description: A vendor specific order source id.
                            type: string
                            example: '#123-MERCH'
                        subtotal:
                            format: double
                            description: The order subtotal.
                            type: number
                            example: 12.34
                        subtotal_usd:
                            format: double
                            description: The order subtotal as fractional US dollars.
                            type: number
                            example: 12.34
                        taxes:
                            format: double
                            description: The taxes applied to the subtotal.
                            type: number
                            example: 0.62
                        taxes_usd:
                            format: double
                            description: The taxes applied to the subtotal in US dollars.
                            type: number
                            example: 0.62
                        insured:
                            description: A flag indicating whether an order is insured. 0 = not insured.
                            type: integer
                            example: 1
                        paid_to_insure:
                            format: double
                            description: Amount of money paid to insure package
                            type: number
                            example: 0.98
                        paid_to_insure_usd:
                            format: double
                            description: Amount of money paid to insure package in US dollars
                            type: number
                            example: 0.98
                        amount_covered:
                            format: double
                            description: Amount of subtotal that is covered by insurance.
                            type: number
                            example: 12.34
                        amount_covered_usd:
                            format: double
                            description: Amount of subtotal that is covered by insurance in US dollars.
                            type: number
                            example: 12.34
                        status:
                            description: The current status of the order.
                            enum:
                                - completed
                                - canceled
                                - failed
                                - created
                            type: string
                            example: failed
                        line_items:
                            description: Line item details stored as a JSON string.
                            type: string
                            example: '{}'
                        exchange_rate:
                            format: double
                            description: Exchange rate that overrides the merchant's default rate.
                            type: number
                            example: 0.034
                        destination:
                            description: The shipping destination for the order.
                            type: string
                            example: 123 Elm St. Seattle WA
                        created_on:
                            description: The created on date as milliseconds since Unix Epoch
                            type: integer
                            example: 10456868456
                        updated_on:
                            description: The updated on date as milliseconds since Unix Epoch
                            type: integer
                            example: 10456868456
                        customer:
                            description: >
                                (Optional) Provide a JSON object to create a Customer specific for this order.
                                If left blank the order email will  be used to look up a customer or a new
                                customer will be created.
                            type: object
                            properties:
                                first_name:
                                    description: Customer's first name.
                                    type: string
                                    example: John
                                last_name:
                                    description: Customer's last name.
                                    type: string
                                    example: John
                                email:
                                    description: Customer's email address.
                                    type: string
                                    example: john@doe.com
                                phone:
                                    description: Customer's phone number.
                                    type: string
                                    example: +1-555-555-3333
                                street_address1:
                                    description: Customer's street address
                                    type: string
                                    example: 123 Elm St.
                                street_address2:
                                    description: Customer's street address
                                    type: string
                                    example: 'Suite #110'
                                city:
                                    description: Customer's city
                                    type: string
                                    example: Seattle
                                province:
                                    description: Customer's province
                                    type: string
                                    example: Washington
                                zip:
                                    description: Customer's zip code
                                    type: string
                                    example: '55555'
                                country_code:
                                    description: Customer's 2-digit country code
                                    type: string
                                    example: US
        required: true
    responses:
        '200':
            description: Returns a success object.
            content:
                application/json:
                    schema:
                        type: object
                        properties:
                            result:
                                description: Result of the operation. Should always be success.
                                type: string
                                example: success
                            id:
                                description: The unique identifier of the order.
                                type: integer
                                example: 123454
    security:
        -
            Merchant Access Token: []
EricWittmann commented 5 years ago

You should be able to accomplish this today by creating a Data Type and then referencing it. That should be functionally equivalent, no?

EricWittmann commented 5 years ago

As for @anatoli26's issue of multiple request bodies - I think you've run up against a bit of a limitation in OpenAPI itself. You cannot create multiple request bodies using the same media type.

BUT...considering this problem more, I think that perhaps the oneOf feature of JSON schema would allow something like this. In your case it might look something like:

post:
    summary: Operation Summary
    description: Operation description.
    operationId: myPostOperation
    requestBody:
        content:
            application/json:
                schema:
                    oneOf: [
                        { "$ref" : "#/components/schemas/UpdateUser'},
                        { "$ref" : "#/components/schemas/UpdateUserAdmin'},
                        { "$ref" : "#/components/schemas/ChangePassword'},
                        { "$ref" : "#/components/schemas/ChangePasswordPRT'},
                        { "$ref" : "#/components/schemas/ChangePasswordAdmin'}
                    ]

You could configure the title and description of each of those referenced schema types. I'm honestly not sure how something like ReDoc would render that, but OpenAPI at least allows it. Of course you could inline all those schemas rather than $ref them, but I would think referencing them would be a better practice.

Sadly Apicurio does not yet support oneOf, but it's something we could try to prioritize if the community valued it.

GimpMaster commented 5 years ago

@EricWittmann - That does work!

The only issue I am seeing is I don't know how to specify a required vs optional parameter when doing it through a reference datatype instead of directly inline as part of the body. If I had to take my preferred method it would be to have an editor directly as part of the Request Body where you can choose Type: Object and add individual items and set required/optional with example values.

EricWittmann commented 5 years ago

Required vs. optional can be set on the property itself when you are defining the global data type:

image

Not sure if that's what you mean or not.

I do agree that Apicurio could use some enhancements in this area. I'm hopeful that in the future things will be more comprehensive without sacrificing ease of use. Right now we've erred more on the side of ease-of-use.

anatoli26 commented 5 years ago

@EricWittmann, thanks for the explanation. OneOf could be a solution for this case, so :+1: for its support!

GimpMaster commented 5 years ago

@EricWittmann Thank you for mentioning how to set a required property on a defined reference datatype. One issue I am seeing is this Issue message.

image

Any thoughts on how I can have a property NOT be an array but still have it required with example in a schema?

EricWittmann commented 5 years ago

This is probably a validation bug introduced with the recent renovation of the validation layer. Can you attach an API definition (minimal definition if possible) that shows this validation error?

GimpMaster commented 5 years ago

Thank you @EricWittmann Attached is a yaml file that has the definition creating the error.

validation_error.txt

EricWittmann commented 5 years ago

Thanks @GimpMaster - however I've imported that api definition into Apicurio and I don't see the validation error:

EDIT: OK nevermind, I was able to reproduce the validation problem. Interestingly it didn't show up when I imported yours, but when I created my own (identical) property then it happened. Mystery! I'll have to dig into this...

GimpMaster commented 5 years ago

Thank you Eric! FYI ApiCurio is awesome. We are using it in our workflows for creating docs, then downloading the yaml and importing into postman for validation testing. It's a great tool, much better than hand writing yaml.

Ranbato commented 5 years ago

We are seeing that same validation error. In our case, if required is put on the individual properties it is ignored by the UI and the validation; if we try to change it, it puts the required fields in an array above the properties and fails validation.

EricWittmann commented 5 years ago

The validation error is tracked in a separate issue here: https://github.com/Apicurio/apicurio-studio/issues/749

Today I tracked down the cause and fixed it. The fix will be included in the next Apicurio release.

Ranbato commented 5 years ago

👍

osamaramihafez commented 2 years ago

So I haven't been able to define an object for some reason and it's starting to give me a headache.

Here's what I have defined in my OAS under the path:

"/an/example/{path}": {
"PUT"{
 "requestBody": {
            "content": {
                "application/x-www-form-urlencode": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "email": {
                                "type": "string"
                            },
                            "password": {
                                "type": "string"
                            },
                            "first_name": {
                                "type": "string"
                            },
                            "last_name": {
                                "type": "string"
                            }
                        }
                    }
                }
            }
        }
     ... more stuff in operation
    }
}

I've also defining the schema elsewhere and referencing it as mentioned above:

"/an/example/{path}": {
"PUT"{
"requestBody": {
                    "$ref": "#/components/requestBodies/PUT_users-id"
                }
    }
}

... stuff in between here

"components": {
    "requestBodies": {
    "PUT_users-id" :{
"content": {
                "application/x-www-form-urlencode": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "email": {
                                "type": "string"
                            },
                            "password": {
                                "type": "string"
                            },
                            "first_name": {
                                "type": "string"
                            },
                            "last_name": {
                                "type": "string"
                            }
                        }
                    }
                }
        }
    }
}

Neither of these seems to work. APIcurito refuses to recognize these schemas of type object :(

At best it gives me the application/format, and this only works when I use the first method (without creating a reference): image

Based on what I've found online, I seem to be doing things correctly, so I don't know what I'm doing wrong. See this link for reference

How can I define a schema of type object if this is incorrect? And if it is the correct formatting, then why won't it appear :')

EricWittmann commented 2 years ago

You are doing things correctly. Unfortunately Studio doesn't currently support inline types like this. Instead you need to define a re-usable type in the Data Types section and then reference it.

osamaramihafez commented 2 years ago

@EricWittmann Isn't that what I did in the second example? By reusable you mean inside of the #/components/requestBodies, correct?

osamaramihafez commented 2 years ago

Nvm, they need to go inside #/components/schemas

EricWittmann commented 2 years ago

Sorry yes - they must go in #/components/schemas as you say. In the editor you need to add them here:

image

Then they are available in the type dropdowns where appropriate. We would like to enhance the editor to allow inline type definitions (obviously supported by the OpenAPI spec) but right now it isn't. :(