Skip to content

[feat][elixir] use ecto schemas and changesets in generated models #21208

New issue

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

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

Already on GitHub? # to your account

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

efcasado
Copy link
Contributor

@efcasado efcasado commented May 4, 2025

Description

This PR extends the Elixir OpenAPI code generator to use Ecto (embeded) schemas for the generated models.

Rather than relying on plain structs or custom validation code, this approach leverages Ecto's built-in validation and casting capabilities to provide a more powerful and idiomatic solution for API client development in Elixir.

The motivation for this change is three-fold:

  • Ecto is widely adopted within the Elixir ecosystem. By aligning the generated code with this de facto standard, we make the output more familiar and maintainable for Elixir developers. Arguably, working with Ecto.Schema and Ecto.Changeset is more idiomatic than easier to reason about that the custom logic currently used by the Elixir generator (eg. generated deserializer module).

  • Improved validation support out of the box: Ecto's validation functions (e.g., validate_required/2, validate_length/3, etc.) enable robust runtime checks for incoming API data, helping catch mismatches early and making client code safer by default.

  • Extensibility: By using Ecto changesets, developers consuming the generated models have a clear and idiomatic extension point for adding custom validation logic or transformation steps, without having to rework the generated code structure. For example, adding validation for a string field with email format would be as simple as patching the generated model with something along the lines of validate_format(:email, ~r/@/).

As usual, curious to hear what you think about this change @wing328 and @mrmstn. I understand this pull-request is more controversial than previous pull-requests, but I believe it helps us elevate the quality of the clients generated by the Elixir generator, making generated clients more idiomatic (read as in accessible) to the wider Elixir community.

Whilst the public API of the generated client does not change, I understand this change could be considered a breaking change. This said and given that the Elixir generator is still to considered to be an alpha version, maybe we can afford targeting master since this change may get the Elixir generator one step closer to leaving its "alpha" status. What do you think?

I am opening this as a draft because whilst the change is fully functional, I am still working on polishing a few rough edges.

What has changed?

  • Switched model generation to use Ecto instead of plain structs and custom deserialization logic. Data casting and validation is handled through Ecto.Changeset functions, eliminating the need for the custom deserializer.ex.mustache template.
  • More consistent type casting and validation across the board.
  • Improved enum handling, which are now enforced using Ecto's validate_inclusion/3 function.
  • Fixed inconsistencies where test inputs did not conform to the OpenAPI spec (e.g., missing required fields or using invalid enum values.).

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 Git BASH)
    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 is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

@efcasado efcasado marked this pull request as ready for review May 4, 2025 12:38
@efcasado efcasado marked this pull request as draft May 4, 2025 14:18
@@ -25,25 +24,28 @@ defmodule DeserializerTest do
"name": "sea"
}
],
"status": "foo"
"status": "available"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The previous version did not validate enum values. This is one of the nice goodies we get from switching to Ecto.

%FormatTest{
integer: 1,
int32: 2,
int64: 3,
number: 4.1,
float: 5.2,
double: 6.3,
decimal: "7.4",
decimal: 7.4,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The previous version did not handle type-casting of decimal values. This is a nice goodie we get from switching to Ecto.

@@ -6,6 +6,7 @@ defmodule OuterEnumTest do

@valid_json """
{
"enum_string_required": "lower",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This shows that the previous implementation was "off", and shows one of the benefits of migrating to Ecto. Now, it's easier and more idiomatic to write proper type-casting and validations, which allows us to have a better Elixir generator.

@efcasado efcasado force-pushed the feat-elixir-ecto-schemas branch 9 times, most recently from d1684c7 to 21c8024 Compare May 13, 2025 07:25
@efcasado efcasado force-pushed the feat-elixir-ecto-schemas branch from 21c8024 to 6dce1e6 Compare May 13, 2025 07:28
@efcasado efcasado force-pushed the feat-elixir-ecto-schemas branch from 6dce1e6 to fa24401 Compare May 13, 2025 07:35
@efcasado efcasado force-pushed the feat-elixir-ecto-schemas branch from fa24401 to 6ad3d78 Compare May 13, 2025 07:43
@efcasado efcasado force-pushed the feat-elixir-ecto-schemas branch from f8684dc to 618e39b Compare May 13, 2025 09:04
@efcasado efcasado force-pushed the feat-elixir-ecto-schemas branch from 618e39b to 3f6e3f3 Compare May 13, 2025 09:08
@efcasado efcasado marked this pull request as ready for review May 16, 2025 16:14
@efcasado
Copy link
Contributor Author

efcasado commented May 16, 2025

@wing328, @mrmstn - I feel this pull request is ready for a first review. Curious to see what you think 😊 It is a significant change, and I recognize it comes with some risk, but I believe it can move us closer to offering a better developer experience for anyone working with clients generated by the Elixir generator.

Do you happen to know of a clean way to dynamically skip generating certain model files? With this PR, I improved enum support by inlining enum values directly into the models where they are used. The previous approach was a bit rough. Models like enum_class) were essentially placeholders with no real value. Ideally, I would like to avoid generating these kinds of empty or redundant files altogether. For now, I’ve updated the model.mustache template to emit empty modules in such cases, but that feels like a workaround. I'm curious if you're aware of a more elegant or supported way to handle this within the generator.

I’m also a bit unsure about the way I handled schemas nested inside maps. While the current solution works, it's limited, and I wonder whether it's worth supporting this at all. These kinds of nested structures, ie. maps of embedded schemas, potentially mixed with arrays, are hard to support generically in a meaningful way. Given that the existing generator doesn’t handle this comprehensively either (unless I am mistaken), I’m leaning toward treating such schemas as plain maps and leaving the responsibility of validation and transformation to developers. It might be better to offer a clear escape hatch than to implement a partial solution that gives a false sense of completeness.

Would love to hear your thoughts on both points.

@efcasado efcasado marked this pull request as draft May 17, 2025 06:16
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant