Skip to content

Conversation

@linxGnu
Copy link
Contributor

@linxGnu linxGnu commented Sep 11, 2025

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    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/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR solves a reported issue, reference it using GitHub's linking syntax (e.g., having "fixes #123" present in the PR description)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

@linxGnu linxGnu changed the title [Rust] [Axum] Support AnyOf, AllOf [Rust-Axum] Support AnyOf, AllOf Sep 11, 2025
@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 11, 2025

Kindly ping @wing328 to review.

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 11, 2025

Please ignore current error: python-lazyImports samples have not been updated yet.

@jacob-mink-1996
Copy link

jacob-mink-1996 commented Sep 11, 2025

Thanks for working on this so quickly @linxGnu - I tested it with my use-case. Looks like I'm no longer getting Box<opaque type> anymore, which is great!

However, I can see a new issue - I'm looking at generating a server against the OpenAI API spec (https://github.com/openai/openai-openapi), and running the following command in order to generate it against the binaries built from this branch.

java -jar ../openapi-generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -i openai.yml -g rust-axum -o ./generated --additional-properties=packageName=generated --openapi-normalizer FILTER="operationId:listModels|retrieveModel|createChatCompletion" --skip-validate-spec

One concern I had was that the stable release of openapi-generator-cli is on my $PATH - could that cause the problem below?

You'll note that the output of this command generates models for createChatCompletion, one of which contains:
{3A51E2DC-4890-4F8A-97D8-C4C719EFE8BD}
In the spec, this structure should actually have a field model that is of type ModelIdsShared - ModelIdsShared IS a generated model, but it appears that it did not get linked from this setup. Is this related to your change?

I believe the confounding factor here is that the CreateChatCompletionRequest object has only one parameter - an allOf that comprises

  1. a $ref to the (properly generated and linked) CreateModelResponseProperties object
  2. an object whose properties should also be added as part of the generated model
    Is that situation supported in this feature?

@wing328
Copy link
Member

wing328 commented Sep 12, 2025

cc @frol (2017/07) @farcaller (2017/08) @richardwhiuk (2019/07) @paladinzh (2020/05) @jacob-pro (2022/10) @dsteeley (2025/07)

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 12, 2025

@jacob-mink-1996
Sorry for my bug, have just fixed it. Could you please double check at your side?

@jacob-mink-1996
Copy link

jacob-mink-1996 commented Sep 12, 2025

@linxGnu at first glance, the generated code looks model-wise correct! Types have been splatted in appropriately. However, I'm seeing lots of errors related to unresolved names:

impl TranscriptTextDeltaEvent {
    fn _name_for_r#type() -> String {
        String::from("TranscriptTextDeltaEvent")
    }

    fn _serialize_r#type<S>(_: &String, s: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        s.serialize_str(&Self::_name_for_r#type())
    }
}

Edit: a little further research - looks like r#IDENTIFIER is legitimate, learn something new every day. However, it's showing up in places where it probably should not, e.g. &Self::_name_for_r#type, #[serde(default = "EvalLogsDataSourceConfig::_name_for_r#type")].

Additionally, I found some models and structs like:

#[derive(Default)]
#[allow(dead_code)]
struct IntermediateRep {
    pub r#type: Vec<String>,
    pub text: Vec<String>,
    pub logprobs: Vec<Vec<models::TranscriptTextDeltaEventLogprobsInner>>,
    pub usage: Vec<models::TranscriptTextUsageTokens>,
    pub r#type: Vec<String>,
}

Here, type is the discrimator for a type A and a type B where Type A is like

TypeA
properties:
type:
type: String,
enum:
- some.value
type_b:
$ref: "#/components/schemas/TypeB"

And type B is much the same. The duplicate r#type field is breaking things here. I'm curious how other languages would have dealt with this.

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 12, 2025

@jacob-mink-1996 please checkout my latest commit bb922e4

I have just patched toVarName which results correct var name for Discriminator, no more _name_for_r#type.

However, there is one compilation error left: validate(nested). I will try fixing it tomorrow.

@linxGnu linxGnu marked this pull request as draft September 12, 2025 17:19
@linxGnu linxGnu marked this pull request as ready for review September 12, 2025 18:22
@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 12, 2025

@jacob-mink-1996

Generated code for https://github.com/openai/openai-openapi works well for me. Please check out latest commit fa93897 and kindly let me know if that works for you as well.

@jacob-mink-1996
Copy link

Getting it built now @linxGnu! Thanks for your support on this, really appreciate how fast you're working. I'll see if the compilation error is something I can manually handle in my generated code and get back to you.

@jacob-mink-1996

This comment was marked as duplicate.

@jacob-mink-1996
Copy link

jacob-mink-1996 commented Sep 12, 2025

@linxGnu
One more thing I found, and I'll consolidate everything in here. Of course, these are just my findings and feedback - take from here what you will!

  1. Single-item anyOf is not generated
    Single-item anyOf was not generated, though it is referenced by other structs
TypeA:
  anyOf:
    - $ref: AnotherType

For now, I'll fix this by adding another item in the spec under this anyOf to get by.

TypeA:
  anyOf:
    - $ref: AnotherType
    - type: string
      title: Never
      description: A field I added to trick the generator

Expected behavior is that TypeA would be an enum like

pub enum TypeA {
  AnotherType(models::AnotherType)
}
  1. Serialized discriminator is unexpected
    For an enum like
ChatCompletionRequestMessage:
  anyOf:
    - $ref: '#/components/schemas/ChatCompletionRequestDeveloperMessage'
    - $ref: '#/components/schemas/ChatCompletionRequestSystemMessage'
    - $ref: '#/components/schemas/ChatCompletionRequestUserMessage'
    - $ref: '#/components/schemas/ChatCompletionRequestAssistantMessage'
    - $ref: '#/components/schemas/ChatCompletionRequestToolMessage'
    - $ref: '#/components/schemas/ChatCompletionRequestFunctionMessage'

I would expect that the discriminator used by serde is the role property from inside each of these. And in fact, that is what we get

#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(tag = "role")]
#[allow(non_camel_case_types)]
pub enum ChatCompletionRequestMessage {
    ChatCompletionRequestDeveloperMessage(models::ChatCompletionRequestDeveloperMessage),
    ChatCompletionRequestSystemMessage(models::ChatCompletionRequestSystemMessage),
    ChatCompletionRequestUserMessage(models::ChatCompletionRequestUserMessage),
    ChatCompletionRequestAssistantMessage(models::ChatCompletionRequestAssistantMessage),
    ChatCompletionRequestToolMessage(models::ChatCompletionRequestToolMessage),
    ChatCompletionRequestFunctionMessage(models::ChatCompletionRequestFunctionMessage),
}

However, that creates a problem for the user of the library, who actually expects this field to be a SPECIFIC enum value.
From the spec

role:
  type: string
  enum:
    - developer
  description: The role of the messages author, in this case `developer`.
  x-stainless-const: true

I think if the serde tag is a specific field and that field is an enum with one value in each option, then the generated code should accept that enum value.

i.e. right now my code from python to the server looks like

messages=[
    { 'role': 'ChatCompletionRequestSystemMessage', 'content': 'You are a pirate who always responds in pirate-speak. You should mention your love of the sea and treasure whenever possible.'},
    { 'role': 'ChatCompletionRequestUserMessage', 'content': 'Hello, how are you?'}
]

I would like the 'role' fields to be 'system', 'user', etc. because those are the single values inside the spec.

e.g.

#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(tag = "role")]
#[allow(non_camel_case_types)]
pub enum ChatCompletionRequestMessage {
    #[serde(rename = "developer")]
    ChatCompletionRequestDeveloperMessage(models::ChatCompletionRequestDeveloperMessage),
    #[serde(rename = "system")]
    ChatCompletionRequestSystemMessage(models::ChatCompletionRequestSystemMessage),
    #[serde(rename = "user")]
    ChatCompletionRequestUserMessage(models::ChatCompletionRequestUserMessage),
    #[serde(rename = "assistant")]
    ChatCompletionRequestAssistantMessage(models::ChatCompletionRequestAssistantMessage),
    #[serde(rename = "tool")]
    ChatCompletionRequestToolMessage(models::ChatCompletionRequestToolMessage),
    #[serde(rename = "function")]
    ChatCompletionRequestFunctionMessage(models::ChatCompletionRequestFunctionMessage),
}

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 13, 2025

@jacob-mink-1996

Concern 1:

Single-item anyOf is not generated
Single-item anyOf was not generated, though it is referenced by other structs
TypeA:
anyOf:
- $ref: AnotherType

Yes, you are right. I can reproduce at my side

  schemas:
    Hello:
      type: object
      properties:
        g:
          $ref: "#/components/schemas/Gum"
    Gum:
      anyOf:
        - $ref: "#/components/schemas/Bubble"
    Bubble:
      type: object
      properties:
        pop:
          type: string

Generated Code:

pub struct Gum { // Blank }

pub struct Bubble {
    pub pop: Option<String>,
}

pub struct Hello {
    pub g: Option<models::Bubble>,
}

@wing328 Please correct me if I am wrong, seem like there is a hidden optimization inside DefaultCodegen when anyOf has only one subschema

In my example, Hello has field g. Instead of g: Option<Gum>, the optimization make it become g: Option<Bubble>. Here is debug info:

CodegenModel {
    name='Gum', 
    schemaName='Gum', .... allParents=null, parentModel=null, children=[], 
    permits=0, anyOf=[], oneOf=[], allOf=[], modelJson='{"$ref" : "#/components/schemas/Bubble"}',
    ...
    requiredVarsMap=null, ref=#/components/schemas/Bubble, schemaIsFromAdditionalProperties=false,
}

@wing328
Copy link
Member

wing328 commented Sep 13, 2025

@wing328 Please correct me if I am wrong, seem like there is a hidden optimization inside DefaultCodegen when anyOf has only one subschema

it's done in openapi normalizer with the rule SIMPLIFY_ONEOF_ANYOF (enabled by default)

https://github.com/openapitools/openapi-generator/blob/master/docs/customization.md#openapi-normalizer

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 13, 2025

That's awesome. Thank you for your insight @wing328

@jacob-mink-1996 as shared by @wing328 above:

SIMPLIFY_ONEOF_ANYOF: when set to true, simplify oneOf/anyOf by 1) removing null (sub-schema) or enum of null (sub-schema) and setting nullable to true instead, and 2) simplifying oneOf/anyOf with a single sub-schema to just the sub-schema itself.

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 13, 2025

@jacob-mink-1996

For your question 2, I don't see any problem.

Specs:

ChatCompletionRequestMessage:
      oneOf:
        - $ref: "#/components/schemas/ChatCompletionRequestDeveloperMessage"
        - $ref: "#/components/schemas/ChatCompletionRequestSystemMessage"
        ...

Generated code:

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[allow(non_camel_case_types)]
pub enum ChatCompletionRequestMessage {
    ChatCompletionRequestDeveloperMessage(models::ChatCompletionRequestDeveloperMessage),
    ChatCompletionRequestSystemMessage(models::ChatCompletionRequestSystemMessage),
    ...
}

Let's analyze:

  • Firstly, ChatCompletionRequestMessage does not define any Discriminator. There is no way to know role should be Discriminator.
  • Secondly, we still need a way to parse & validate the request. Therefore, we utilized serde enum#untagged:

There is no explicit tag identifying which variant the data contains. Serde will try to match the data against each variant in order and the first one that deserializes successfully is the one returned.

  • Lastly, one thing I agree: untagged is not perfect for oneOf, because it can not sanitize invalid requests, which match with at least 2 variants.

However, thank to comprehensive features of OpenAPI Generator, here is the solution, specially for your case:

  • Please checkout my latest commit 5d96de6
  • Field role inside ChatCompletionRequestDeveloperMessage or ChatCompletionRequestSystemMessage is inline-enum with single variant:
ChatCompletionRequestDeveloperMessage:
   properties:
        role:
          type: string
          enum:
            - developer
          description: The role of the messages author, in this case `developer`.
  • You should turn-on feature RESOLVE_INLINE_ENUMS=true as described here. Command:
openapi-generator generate --skip-validate-spec --enable-post-process-file --inline
-schema-options RESOLVE_INLINE_ENUMS=true ....
  • Generated code looks awesome:
pub struct ChatCompletionRequestDeveloperMessage {
    #[serde(rename = "role")]
    #[validate(nested)]
    pub role: models::ChatCompletionRequestDeveloperMessageRole,
    ...
}

pub enum ChatCompletionRequestDeveloperMessageRole {
    #[serde(rename = "developer")]
    Developer,
}

@jacob-mink-1996
Copy link

Thank you @linxGnu - I believe in your latest, the _serialize_r_type generation came out differently than before, which is causing a build error. I think it takes an incorrect first parameter &String that should be &T.
I've made note of the arguments to the generator, though, and plan on using those once this PR has merged.

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 14, 2025

@jacob-mink-1996 could you please share your build error and ensure that you are building openapi codegen using my latest commit 5d96de6.

Noted:

Command:

openapi-generator generate --skip-validate-spec --enable-post-process-file --inline
-schema-options RESOLVE_INLINE_ENUMS=true \
        -i openapi.yaml \
        -g rust-axum \
        -o openai \
        --package-name openai

Tests passed:
Screenshot 2025-09-14 at 10 38 24

@jacob-mink-1996
Copy link

@linxGnu thanks for the info. Looks like my environment was dirty - all is well. Not that I'm a maintainer, but I'm signing off at least for my use case :) Thanks again for your hard work here.

Another question that may not be related (I can make a new issue if need be). For an endpoint that has more than one response type specified, shouldn't the oneOf mechanism be applied to those when generating the api response types, as well?

e.g.

post:
  operationId: createChatCompletion
  tags:
    - Chat
  summary: Create chat completion
  requestBody:
    required: true
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/CreateChatCompletionRequest'
  responses:
    '200':
      description: OK
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/CreateChatCompletionResponse'
        text/event-stream:
          schema:
            $ref: '#/components/schemas/CreateChatCompletionStreamResponse'

would yield an enum like

// in api
pub enum CreateChatCompletionResponse {
  Status200_OK(models::NewEnumNotGenerated)
}

// in models
pub enum NewEnumNotGenerated {
  ApplicationJson(models::CreateChatCompletionResponse),
  EventStream(models::CreateChatCompletionStreamResponse),
}

If that's a different concept than the one you are working on in this PR, I'll make a new ticket.

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 15, 2025

@jacob-mink-1996 yes, it's another matter, please create a new ticket for it. Just fyi, only application/json is fully supported for now.

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 15, 2025

@wing328 I think it's ready to review & merge. Please ignore CI Error due to Rust Servers* (not related to Rust Axum)

@jacob-mink-1996
Copy link

jacob-mink-1996 commented Sep 15, 2025

@linxGnu Tried with your very latest. The enum for ChatCompletionRequestMessage is not working on the latest - it is still generating

#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
#[serde(tag = "role")]
#[allow(non_camel_case_types, clippy::large_enum_variant)]
pub enum ChatCompletionRequestMessage {    
    ChatCompletionRequestDeveloperMessage(models::ChatCompletionRequestDeveloperMessage),
    ChatCompletionRequestSystemMessage(models::ChatCompletionRequestSystemMessage),
    ChatCompletionRequestUserMessage(models::ChatCompletionRequestUserMessage),
    ChatCompletionRequestAssistantMessage(models::ChatCompletionRequestAssistantMessage),
    ChatCompletionRequestToolMessage(models::ChatCompletionRequestToolMessage),
    ChatCompletionRequestFunctionMessage(models::ChatCompletionRequestFunctionMessage),
}

and not placing any #[serde(rename = "...")] on the variants. I tested with the same command you wrote in your comment above - if you still succeed to generate this correctly, I'll wait until the PR merges and I can grab a snapshot.

FYR the command I was trying to use is

java -jar ../openapi-generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -i ../spec/openai.yml -g rust-axum -o server_generated --additional-properties=packageName=server_generated --openapi-normalizer FILTER="operationId:listModels|retrieveModel|createChatCompletion",SIMPLIFY_ONEOF_ANYOF=false --enable-post-process-file --inline-schema-options RESOLVE_INLINE_ENUMS=true

NOTE: after manual modification, the only way I could get the enum to deserialize correctly was by making it #[serde(untagged)] - which seems to conflict with the earlier situation. Interestingly, the spec I am working with DOES specify a discriminator on this enum.

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 16, 2025

@jacob-mink-1996

There is another issue

What was fixed

  • When discriminator is non-string (as above case, role is the discriminator, type is enum) -> set to [serde(untagged)]

TL;DR

  • Please checkout my gist: https://gist.github.com/linxGnu/790ba82424a44f8d6013ad899667b163
  • Current generated code is valid. Thus, [serde(untagged)] is correct
  • Placing #[serde(rename = "...")] on the variants as your expectation is incorrect
  • Run my gist at your local, you will see serialize/deserialize work perfectly for generated code. Please look at stdout carefully

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 26, 2025

@jacob-mink-1996 Do you have any other concerns

@wing328
Copy link
Member

wing328 commented Sep 26, 2025

I'll merge it before the release this weekend.

Thanks for the PR

Have a nice weekend

@linxGnu
Copy link
Contributor Author

linxGnu commented Sep 26, 2025

Thank you @wing328

@wing328 wing328 added this to the 7.16.0 milestone Sep 27, 2025
@wing328 wing328 merged commit e38f6c0 into OpenAPITools:master Sep 27, 2025
18 checks passed
@linxGnu linxGnu deleted the support_anyof_allof branch September 29, 2025 04:54
rajvesh pushed a commit to rajvesh/openapi-generator that referenced this pull request Dec 25, 2025
* Support AnyOf, AllOf

* Update

* Fix

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Update
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.

3 participants