Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] Add x-is-free-form vendor extension #6849

Merged
merged 2 commits into from
Aug 24, 2020
Merged

Conversation

jimschubert
Copy link
Member

This adds an x-is-free-form vendor extension to allow users to skip our
"free-form" logic which would previously prevent object schemas with no
properties to be considered "free-form". The previous behavior was due
in part to Swagger Parser not exposing additionalProperties: false to
us (which should be similar behavior to this extension).

A free-form object is considered a dynamic object with any number of
properties/types. DefaultGenerator does not allow for generation of
models considered free-form. However, a base type with no properties and
no additional properties is allowed by OpenAPI Specification and is
meaningful in many languages (e.g. "marker interfaces" or abstract
closed types).

PR checklist

  • Read the contribution guidelines.
  • If contributing template-only or documentation-only changes which will change sample output, build the project beforehand.
  • Run the shell script ./bin/generate-samples.shto update all Petstore samples related to your fix. This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master. These must match the expectations made by your contribution. You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/config/java*. For Windows users, please run the script in Git BASH.
  • File the PR against the correct branch: master
  • Copy the technical committee to review the pull request if your PR is targeting a particular programming language.

cc @OpenAPITools/generator-core-team

This adds an x-is-free-form vendor extension to allow users to skip our
"free-form" logic which would previously prevent object schemas with no
properties to be considered "free-form". The previous behavior was due
in part to Swagger Parser not exposing `additionalProperties: false` to
us (which should be similar behavior to this extension).

A free-form object is considered a dynamic object with any number of
properties/types. DefaultGenerator does not allow for generation of
models considered free-form. However, a base type with no properties and
no additional properties is allowed by OpenAPI Specification and is
meaningful in many languages (e.g. "marker interfaces" or abstract
closed types).
@jimschubert jimschubert added this to the 5.0.0 milestone Jul 3, 2020
@auto-labeler
Copy link

auto-labeler bot commented Jul 3, 2020

👍 Thanks for opening this issue!
🏷 I have applied any labels matching special text in your issue.

The team will review the labels and make any necessary changes.

@jeff9finger
Copy link
Contributor

I think the definition of a free form object should be reconsidered here. Here is the definition from above:

A free-form object is considered a dynamic object with any number of properties/types.

I have a case where object has no properties (and therefore no discriminator property), but is referenced from another model's allOf. In this case, I don't think the referenced object should be considered free-form.

But perhaps I don't have the complete picture.

Here is a code snippet that I have in my branch - ModelUtils#isFreeFormObject(). I added the section with the comments "also check to see if this object is referenced from another allOf, anyOf, oneOf"

    public static boolean isFreeFormObject(OpenAPI openAPI, Schema schema) {
        if (schema == null) {
            // TODO: Is this message necessary? A null schema is not a free-form object, so the result is correct.
            once(LOGGER).error("Schema cannot be null in isFreeFormObject check");
            return false;
        }

        // not free-form if allOf, anyOf, oneOf is not empty
        if (schema instanceof ComposedSchema) {
            ComposedSchema cs = (ComposedSchema) schema;
            List<Schema> interfaces = ModelUtils.getInterfaces(cs);
            if (interfaces != null && !interfaces.isEmpty()) {
                return false;
            }
        }
 
        // also check to see if this object is referenced from another allOf, anyOf, oneOf
        Map<String, Schema> schemas = getSchemas(openAPI);
        if (schemas != null && !schemas.isEmpty()) {
            for (Schema s : schemas.values()) {
                if (!s.equals(schema)) {
                    if (s instanceof ComposedSchema) {
                        ComposedSchema cs = (ComposedSchema) s;
                        List<Schema> interfaces = ModelUtils.getInterfaces(cs);
                        if (isReferenceToSchema(openAPI,interfaces, schema)) {
                            break;
                        }
                    }
                }
            }
        }

        // has at least one property
        if ("object".equals(schema.getType())) {
            // no properties
            if ((schema.getProperties() == null || schema.getProperties().isEmpty())) {
                Schema addlProps = getAdditionalProperties(openAPI, schema);
                // additionalProperties not defined
                if (addlProps == null) {
                    return true;
                } else {
                    if (addlProps instanceof ObjectSchema) {
                        ObjectSchema objSchema = (ObjectSchema) addlProps;
                        // additionalProperties defined as {}
                        if (objSchema.getProperties() == null || objSchema.getProperties().isEmpty()) {
                            return true;
                        }
                    } else if (addlProps instanceof Schema) {
                        // additionalProperties defined as {}
                        if (addlProps.getType() == null && (addlProps.getProperties() == null || addlProps.getProperties().isEmpty())) {
                            return true;
                        }
                    }
                }
            }
        }

        return false;
    }

    private static boolean isReferenceToSchema(OpenAPI openAPI, List<Schema> schemas, Schema schema) {
        if (schemas != null) {
            for (Schema s : schemas) {
                String ref = s.get$ref();
                if (StringUtils.isNotEmpty(ref)) {
                    Schema referencedSchema = getSchema(openAPI, getSimpleRef(ref));
                    if (schema.equals(referencedSchema)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

Does this violate the semantics of a "free-form object"?

@jimschubert
Copy link
Member Author

@jeff9finger I should be at least partially addressing your concern about free-form definition in this PR by removing the requirement that a model be considered a model if it has at least one property.

I like the suggestion to also consider if the schema is referenced in a composed schema, but I'll need to look at it more to understand if there are any hidden scenarios.

@jimschubert
Copy link
Member Author

jimschubert commented Jul 7, 2020

@wing328 any idea how to tackle this efficiently?

I think we could disallow "Free-form" objects as being allowed in oneOf/anyOf/allOf because we don't generate free-form objects. That proposed solution could only help.

I still think we should have the vendor extension because as a user I'd rather have an empty model created than a compilation error for a valid OpenAPI document.

The issue that I have is that all of these result in properties=null and additionalProperties=null:

definitions:
  Command:
    title: Command
    description: The base object for all command objects.
    type: object
    additionalProperties: false
definitions:
  Command:
    title: Command
    description: The base object for all command objects.
    type: object
    properties: {}
    additionalProperties: {}
definitions:
  Command:
    title: Command
    description: The base object for all command objects.
    type: object
    properties: {}
    additionalProperties: null

imho the 3 schemas above should not be considered free-form objects, but we currently can't ensure this because swagger-parser doesn't properly handle the false condition and returns nulls when the schemas have no definitions.

@@ -65,6 +65,8 @@
// A vendor extension to track the value of the 'disallowAdditionalPropertiesIfNotPresent' CLI
private static final String disallowAdditionalPropertiesIfNotPresent = "x-disallow-additional-properties-if-not-present";

private static final String freeFormExplicit = "x-is-free-form";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use FREE_FORM_EXPLICIT to match Java style for constants?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to do this whenever we get more caught up on pull requests, but matched other constant styles in the file to avoid potential merge conflicts elsewhere.

We do track Sonar and this is a rule that's evaluated.

@jeff9finger
Copy link
Contributor

@jimschubert Thanks for the analysis.

imho the 3 schemas above should not be considered free-form objects

So, only schemas that don't define type: object nor additionalProperties would be considered free-form, unless the vendor extension is true?

@jimschubert
Copy link
Member Author

@jeff9finger for clarification, here are the rules I think would make sense:

  • Additional properties that are empty with null properties would be "free-form"
  • Additional properties = null and properties = null would be "free-form"
  • All others are valid objects that generate, providing a warning if additional properties is non-empty

The issue I'm seeing is that swagger-parser isn't giving us that empty object state, and it's not handling additionalProperties=false. I'll need to dig into it a little more to see if there's a workaround.

@jimschubert
Copy link
Member Author

pinging @OpenAPITools/generator-core-team again

@spacether
Copy link
Contributor

spacether commented Aug 3, 2020

So I disagree with the description that this schema is not free form:

definitions:
  Command:
    title: Command
    description: The base object for all command objects.
    type: object
    properties: {}
    additionalProperties: {}

You mention that our codegen incorrectly sets properties = null and additionalProperties = null for this case.
additionalProperties: {} should generate an empty Schema and allow any string key names with any type values for this model. This is what currently happens for v3 specs when additionalProperties is absent or True.
Our codegen does this because swagger parser does not correctly handle additionalProperties for v2 specs, right?

Free form objects could be generated and used as marker interfaces/abstract closed types.
How about:

  • turning on free form model generation all the time or only when it is used as an interface schema
  • adding a pre-processor or postprocessor step which fixes the additionalProperties results for v2 specs
    • additionalProperties absent -> additionalProperties = {} (consistent with v3 parsing + the swagger spec)
    • additionalProperties = True -> additionalProperties = {} (consistent with v3 parsing + the swagger spec)
    • additionalProperties = False -> additionalProperties = null (consistent with v3 parsing + the swagger spec)

This makes it so:

  • additionalProperties handling will be consistent across v2 and v3 input specs
  • we don't have to add another CLI flag
  • interface types will automatically work for our users without them having to learn about/use another CLI option

I also like the Idea of seeing if a free-form schema is used in another schema, and if it is, generating it based upon that usage.

Shoot, do we not have a pre-processing step before specs are parsed?
I'm not able to find one in our code base and v2 specs come back from SwaggerParseResult result = new OpenAPIParser().readLocation(inputSpec, authorizationValues, options);
as v3 specs, and none of the original v2 spec spec json/yaml information is preserved as a schema field.

@sebastien-rosset do you have any thoughts on this issue?

@sebastien-rosset
Copy link
Contributor

sebastien-rosset commented Aug 3, 2020

I think the definition of a free form object should be reconsidered here. Here is the definition from above:

A free-form object is considered a dynamic object with any number of properties/types.

I have a case where object has no properties (and therefore no discriminator property), but is referenced from another model's allOf. In this case, I don't think the referenced object should be considered free-form.

@jeff9finger , there is also a "isAnyType" method, would this help for your use case? IsAnyType means the type can be any valid JSON type, i.e. it can be the object, number, array, string or boolean. With OAS 3.1, it can also be the null type. On the other hand, isFreeForm means the type must be object. So an array, number, string, boolean or null type are not considered free-form objects. I'm just starting the read this thread, I don't have the full context yet.

@sebastien-rosset
Copy link
Contributor

So I disagree with the description that this schema is not free form:

definitions:
  Command:
    title: Command
    description: The base object for all command objects.
    type: object
    properties: {}
    additionalProperties: {}

I'm slowly catching up with this thread, @spacether I agree with you the above is a free-form object (without having read the entire thread). That's because 1) the type is object and 2) there are no explicit (declared) properties and no restrictions on the additional properties. So basically the value can be any arbitrary map.

@spacether
Copy link
Contributor

spacether commented Aug 3, 2020

FYI, I just submitted a PR to fix additionalProperties handling in v2 specs in swagger-parser at: swagger-api/swagger-parser#1414
If merged we need:

  • a later PR to have the latest mater version of swagger-parser use the new v1 branch of swagger-parser which parses v2 specs.
  • a PR in our project to pull in the latest version of swagger-parser (master branch) +
  • update our modelutils to define freeform object type using the correct results from additional properties

If merged swagger-api/swagger-parser#1414 will mean that additionalProperties will be handed the same in v2 and v3 specs in our project.
For those who don't know OpenapiGenerator converts all input v2 specs into v3 specs under the hood.

@jimschubert
Copy link
Member Author

@spacether did you test this branch with that proposed change? I think this PR might still be necessary for example where specs are generated by tooling and that tooling is based on older versions of swagger which don't properly handle additionalProperties.

* master: (121 commits)
  [PowerShell] better publishing workflow (#7114)
  [aspnetcore] Typo issues in docs and generated code (#7094)
  fix http signaure auth in build.sbt (#7110)
  fix for the issue facing spec invlolving arrayschema structure with ref (#6310)
  [csharp-netcore] added cancellation tokens to async calls (#7077)
  [PS] Allow CI to publish the module (#7091)
  [Dart] Treat float as double (#6924)
  [Java][jersey2]Fix gradle HttpSignatureAuth dependencies (#7096)
  move maven,gradle tests to shipppable ci (#7108)
  [MARKDOWN] Fix issue 6089 with property and parameter names (#6105)
  [BUG] Multi-Part content type in response ignores properties of composed schema (allOf/oneOf) (#6073)
  [online] Fix for version conflicts with springfox/boot (#7102)
  skip some installations to shorten build time
  [Go][Exp] better code format (#7088)
  Fix Shippable CI (#7097)
  typescript-node: clean up require and import (#6947)
  commented out perl, bash tests to reduce build time
  Add a link to conference paper (#7086)
  Add a link to the blog post at qiita (#7084)
  migrate typescript.sh to new config format (#7078)
  ...
@spacether
Copy link
Contributor

spacether commented Aug 4, 2020

@spacether did you test this branch with that proposed change? I think this PR might still be necessary for example where specs are generated by tooling and that tooling is based on older versions of swagger which don't properly handle additionalProperties.

No I haven't tested this branch with my proposed change yet.
I don't know how to because the latest version of swagger parser uses a pom.xml release version to pull in v2 parsing code. And my PR is not yet released.

Are these PRs coupled?
My PR makes it so the results from v2 and v3 specs relative to including additionalProperties is consistent.
So if you have tests samples for v3 specs, and they work then my PR is not coupled to this one.

Is this PR functionally equivalent to turning on generateAliasAsModel?
Aren't these object models that include any types aliases to Map?
Could that generated alias map model meet people's marker interface needs?

@jimschubert
Copy link
Member Author

So I disagree with the description that this schema is not free form:

definitions:
  Command:
    title: Command
    description: The base object for all command objects.
    type: object
    properties: {}
    additionalProperties: {}

I'm slowly catching up with this thread, @spacether I agree with you the above is a free-form object (without having read the entire thread). That's because 1) the type is object and 2) there are no explicit (declared) properties and no restrictions on the additional properties. So basically the value can be any arbitrary map.

To clarify the issues:

  1. OAS defines additionalProperties as:
    image
    meaning if additionalProperties does not exist, it should default to true (meaning all objects are freeform)
  2. swagger-parser does not have a case for additionalProperties: false
  3. users have historically gotten around this in ours and other tooling by defining an empty object as additionalProperties: {}
  4. OAI does not explicitly say that a Schema with no properties is inherently free-form, if anything this is implied by the defaulted additionalProperties: true for which there is no option for false, null in the object model implies that it hadn't been set (and therefore defaults to true); this leaves us only with the empty object option
  5. JSON Schema requires that when additionalProperties is an object, the resulting object validates against all properties in the object:
    image
  6. The additionalProperties: false case is fundamentally broken anyway.

So, I think a vendor extension is the cleanest approach.

@jimschubert
Copy link
Member Author

Let's merge this and revisit when Justin's PR in swagger-parser gets merged.

@jimschubert jimschubert merged commit a97feaf into master Aug 24, 2020
@jimschubert jimschubert deleted the allow-empty-models branch August 24, 2020 23:00
jimschubert added a commit to jirikuncar/openapi-generator that referenced this pull request Aug 31, 2020
* master: (355 commits)
  [php-slim4] Move config to a separate file (OpenAPITools#6971)
  [C][Client][Clang Static Analyzer] Remove the useless free operation for (OpenAPITools#7309)
  Fix typescript-node generation when only models are generated (OpenAPITools#7127)
  update spring config to use java8 (OpenAPITools#7308)
  [C][Client][Clang Static Analyzer] Fix uninitialized argument value (OpenAPITools#7305)
  [Java] remove deprecated jackson classes (OpenAPITools#7304)
  Adds generator unaliasSchema method, uses it to refactor python-experimental (OpenAPITools#7274)
  [Rust][reqwest] prefix local variables with "local_var" (OpenAPITools#7299)
  [Java][jersey2]Support enum discriminator value in child objects (OpenAPITools#7267)
  [C][Client][Clang Static Analyzer] Fix memory leak before function returnning (OpenAPITools#7302)
  add extension for hashable in swift5 (OpenAPITools#7300)
  [kotlin][client] fix warning Extension is shadowed by a member (OpenAPITools#7286)
  Add wbt-solutions logo (OpenAPITools#7298)
  [c-sharp] Fix default values with discriminator  (OpenAPITools#7296)
  Add x-is-json to csharp generators (OpenAPITools#7293)
  Add raksul (OpenAPITools#7295)
  Add wbt-solutions as using company (OpenAPITools#7292)
  [C][Client][Clang Static Analyzer] Fix memory leak in apiClient_invoke (OpenAPITools#7285)
  [Java][clients] remove java.lang prefix from Object (OpenAPITools#6806)
  [core] Add x-is-free-form vendor extension (OpenAPITools#6849)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants