Skip to content

Azure Key Vault case insensitive support and dash-underscore translation #607

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

d15ky
Copy link
Contributor

@d15ky d15ky commented Apr 23, 2025

Add case-insensitive and dash-to-underscore support for Azure Key Vault settings source

This PR improves the AzureKeyVaultSettingsSource making it more flexible and reliable when working with Azure Key Vault.

Note: This PR includes a temporary copy of the bugfix proposed in #608 to ensure tests pass. Once that PR is merged, this one can be rebased cleanly onto main.

Summary of changes

  1. Support for case-insensitive mode

Prior to the recent update on Friday (version 2.9.0+), the source behaved case-insensitively by default. That behavior was unintentionally broken with the latest changes. This PR restores the original default behavior while making the logic explicit and consistent with the EnvironmentSettingsSource, i.e. AzureKeyVaultSettingsSource now accepts case_sensitive option.

While implementing this, an issue was found in the inherited EnvironmentSettingsSource, which did not correctly process nested models with validation aliases in case-insensitive mode. The related test has been moved to a separate PR to keep concerns isolated.

  1. New dash_to_underscore option (enabled by default)

    A dash_to_underscore option has been added to translate between dashes (-) in Azure Key Vault secret names and underscores (_) in model field names. This mapping is enabled by default and is safe, as:

    • Python identifiers cannot contain dashes.
    • Azure Key Vault secret names cannot contain underscores.

    The option can be disabled for cases where this behavior is not desired though I cannot think of any.

All tests pass, linting is clean, and the behavior is covered by updated test cases. Code style and structure were followed closely to match existing patterns in both the implementation and tests.

Examples

Dash-to-underscore translation:

from pydantic_settings import BaseSettings
from pydantic_settings.sources import AzureKeyVaultSettingsSource

class Settings(BaseSettings):
    my_field: str

settings = Settings(_secrets_sources=[
    AzureKeyVaultSettingsSource(vault_url="https://my-vault.vault.azure.net/")
])
# Retrieves the 'my-field' secret and maps it to 'my_field'

@hramezani
Copy link
Member

Thanks @d15ky for the PR.

While implementing this, an issue was found in the inherited EnvironmentSettingsSource, which did not correctly process nested models with validation aliases in case-insensitive mode. The related test has been updated to include such a case and now passes with the changes.

Is it possible to take these changes out and make another PR? Also, please don't update the existing test and add a new tests.

@d15ky
Copy link
Contributor Author

d15ky commented Apr 23, 2025

@hramezani thanks for the quick review! Sure, I'll move the bugfix to a separate PR.

Regarding the test: I updated it because its name, test_case_insensitive_nested_optional, seemed consistent with the changes. That said, I'll revert the modification and add a new test in the separate PR instead.

@d15ky
Copy link
Contributor Author

d15ky commented Apr 23, 2025

@hramezani I've separated the bugfix into a dedicated PR.

I've removed the test changes from this one, but kept the bugfix logic itself, as it's required for the AKV tests to pass with the new case-insensitive functionality.

Let me know if anything else needs adjusting!

@hramezani
Copy link
Member

Thanks @d15ky for updating the PR.

@AndreuCodina could you review the PR?

@AndreuCodina
Copy link
Contributor

AndreuCodina commented Apr 24, 2025

It's a complex topic.

Dash to underscore was in my initial Key Vault source, but I removed it. In the official provider of Key Vault, 2 dashes are used for nesting (https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-9.0#secret-storage-in-the-production-environment-with-azure-key-vault) or arrays (https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-9.0#bind-an-array-to-a-class), and 1 dash can be used for prefixes (https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-9.0#use-a-key-name-prefix), and they are stripped.

As we use .NET and Python, we use PascalCase secrets in Key Vault, and we are able to read MySecret from .NET and Python. With your secret convention, you can only use Azure Key Vault with 1 provider for a specific language (Python) instead of using more programming languages, and that's not a reasonable enterprise solution.

These are our settings:

class ApplicationSettings(BaseSettings):
    model_config = SettingsConfigDict(alias_generator=to_pascal)
    
    sql_connection_string: str

I guess you don't use PascalCase outside the Key Vault source, so you can't apply alias_generator to all fields, and instead you'd have to use alias for the secrets from Azure Key Vault.

I also don't like boilerplate, but you have to understand your solution is valid, but "-" in Key Vault is usually a special character. For example, Spring Boot works with ".", and with the Key Vault provider, "-" is replaced with "." (https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/secret-management#use---instead-of--in-secret-names). So, in that ecosystem, "-" doesn't mean "different word".

TBH I'd prefer a translator from PascalCase to snake_case instead of replacing a special character, because I think PascalCase should be the standard in Azure Key Vault.

@d15ky
Copy link
Contributor Author

d15ky commented Apr 24, 2025

@AndreuCodina Thank you for the detailed response. I'm not sure if I’ve fully understood all the concerns, so I’ll echo my understanding and provide counterpoints to clarify my intent.

  1. Dashes for nesting, arrays, and prefixes

The proposed solution doesn’t interfere with that behavior. For example, a double dash in my implementation would still correctly map to a nested model. The transformation is exclusively applied when Pydantic searches for a field to map to — it doesn’t affect aliases, nesting logic, or any other part of the resolution process.

  1. PascalCase and language interoperability

I’m not proposing to replace PascalCase conventions — this is an opt-in translation that only affects Python variable names (not aliases) and only during the final map search when other approaches failed. In your example setup, using PascalCase secrets with alias_generator=to_pascal would continue to work as intended.

I agree that there’s a risk of the Python side of the team assuming a specific dash convention, but since:

  • dashes appear to be open to interpretation (e.g., Spring Boot translating them to dots),
  • and since pydantic-settings doesn't support writing to Key Vault (only reading),

it seems like the real risk here lies more in a lack of team-wide naming conventions than in pydantic’s behavior. This translation doesn’t enforce or imply a naming standard on the Key Vault itself — a badly named KeyVault variable that would break the flow of .NET side still needs to be created manually.

  1. Spring Boot example

To me, it actually supports the idea that dashes are flexible and interpreted differently depending on the ecosystem — which makes it reasonable for Python tooling to offer a translation strategy tailored to Python conventions. I don't see any other way dashes can be interpreted by Python in this context except as delimiters. Once again, nesting logic is unaffected by this translation and would take precedence in resolution.


So if I understand correctly, the core question is not about technical correctness or risk of breakage — it's about whether this kind of client-side, read-only convention should be supported by pydantic-settings to accommodate Pythonic naming patterns. The transformation itself is safe, isolated, and opt-in, and its inclusion would simply acknowledge a common use case without enforcing it.

Since pydantic-settings is read-only and requires manual creation of secrets anyway, I think it’s reasonable to offer this behavior as a tool — leaving it up to teams to apply it (or not) based on their context.

With that said, I’m open to remove the feature entirely if needed, especially since most of this PR is focused on case-insensitivity. Alternatively, I could split it into a separate PR to continue the discussion separately.

@AndreuCodina
Copy link
Contributor

AndreuCodina commented Apr 24, 2025

@AndreuCodina Thank you for the detailed response. I'm not sure if I’ve fully understood all the concerns, so I’ll echo my understanding and provide counterpoints to clarify my intent.

  1. Dashes for nesting, arrays, and prefixes

The proposed solution doesn’t interfere with that behavior. For example, a double dash in my implementation would still correctly map to a nested model. The transformation is exclusively applied when Pydantic searches for a field to map to — it doesn’t affect aliases, nesting logic, or any other part of the resolution process.

  1. PascalCase and language interoperability

I’m not proposing to replace PascalCase conventions — this is an opt-in translation that only affects Python variable names (not aliases) and only during the final map search when other approaches failed. In your example setup, using PascalCase secrets with alias_generator=to_pascal would continue to work as intended.

I agree that there’s a risk of the Python side of the team assuming a specific dash convention, but since:

  • dashes appear to be open to interpretation (e.g., Spring Boot translating them to dots),
  • and since pydantic-settings doesn't support writing to Key Vault (only reading),

it seems like the real risk here lies more in a lack of team-wide naming conventions than in pydantic’s behavior. This translation doesn’t enforce or imply a naming standard on the Key Vault itself — a badly named KeyVault variable that would break the flow of .NET side still needs to be created manually.

  1. Spring Boot example

To me, it actually supports the idea that dashes are flexible and interpreted differently depending on the ecosystem — which makes it reasonable for Python tooling to offer a translation strategy tailored to Python conventions. I don't see any other way dashes can be interpreted by Python in this context except as delimiters. Once again, nesting logic is unaffected by this translation and would take precedence in resolution.

So if I understand correctly, the core question is not about technical correctness or risk of breakage — it's about whether this kind of client-side, read-only convention should be supported by pydantic-settings to accommodate Pythonic naming patterns. The transformation itself is safe, isolated, and opt-in, and its inclusion would simply acknowledge a common use case without enforcing it.

Since pydantic-settings is read-only and requires manual creation of secrets anyway, I think it’s reasonable to offer this behavior as a tool — leaving it up to teams to apply it (or not) based on their context.

With that said, I’m open to remove the feature entirely if needed, especially since most of this PR is focused on case-insensitivity. Alternatively, I could split it into a separate PR to continue the discussion separately.

Your solution works, but:

  • AFAIK, a single dash is not used in any current provider to represent the separation between two words (e.g. "MySecret" is "My Secret"). Instead, a dash represents a special case in any provider, even in Spring Boot, so this provider would be the first to say "you can split words with dashes".
  • With case insensitivity, a lot of languages can read from PascalCase, camelCase or lowercase, so I don't think naming the secrets with one dash is a good idea, and the PR tries to make a user-friendly solution the rare case of using a dash to represent the separation between words.

Meanwhile your solution prevents the use of alias_generator or alias and offers a cleaner solution, it forces a naming convention in Key Vault that isn't compatible or shared by other programming languages, so my personal opinion would be to stop using that convention and introduce a feature in pydantic-settings for, when a capital letter is detected, change it to a lowercase letter + underscore if it's not the first letter. For example: SqlConnectionString|sqlConnectionString -> sql_connection_string.

FYI in our case, we use PascalCase for Key Vault, .env files and App Configuration. And, as we use to_pascal, the environment variables map seamlessly. It's easy and reduces development time.

But still, if you want to have this feature, I accept it.

@d15ky
Copy link
Contributor Author

d15ky commented Apr 24, 2025

@AndreuCodina thanks again for the quick response.
I have nothing to add — I agree with you on every point:

  • I agree that it is better to follow the Key Vault naming convention. My personal situation is that, at my company, the naming strategy with dashes is already used across different parts of a large project, and I cannot enforce changing it.

  • I also agree that it would be better to have a general feature like the one you proposed, using camelCase or PascalCase translation. As another example of a general feature that would solve my use case, it would be great to have source-specific alias generators.

My motivation was that I thought I might not be the only one with this specific situation, so I wanted to contribute an option for others who might have the same problem. It is an optional and non-breaking extension of functionality — though I agree it may give the wrong impression about how secrets in Key Vault should be named.

I think it’s best to leave the decision to the maintainers. If the aim of pydantic-settings is to offer maximum flexibility, it makes sense to keep the option. If the goal is to promote consistent conventions, it may be better to drop it.
One possible compromise could be to disable the feature by default and require users to enable it explicitly, so it’s clear that it’s an intentional decision rather than an implicit default. This way, versatility is preserved, while gently nudging users toward the recommended conventions.

Thanks again for the thoughtful discussion.

@hramezani
Copy link
Member

Thanks both for your contributions here.

I am not an expert in KeyVault. But let's disable the new behavior by default, and the user enables it.
Also, we need to document it to let the user know the behavior exists and how it can be enabled.

@AndreuCodina are you happy with my proposed approach?

@AndreuCodina
Copy link
Contributor

AndreuCodina commented Apr 28, 2025

Thanks both for your contributions here.

I am not an expert in KeyVault. But let's disable the new behavior by default, and the user enables it. Also, we need to document it to let the user know the behavior exists and how it can be enabled.

@AndreuCodina are you happy with my proposed approach?

About the topic above, yes.

Apart from that, I don't know exactly what are the real implications of removing the try...except with the KeyError.

@AndreuCodina
Copy link
Contributor

One thing to add @d15ky, Key Vault should always be case-insensitive:

Objects are uniquely identified within Key Vault using a case-insensitive identifier called the object identifier. [...] Key Vault object identifiers are also valid URLs, but should always be compared as case-insensitive strings.

https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#object-identifiers

@d15ky
Copy link
Contributor Author

d15ky commented Apr 28, 2025

@AndreuCodina I removed it because it was masking the real exception thrown by the Azure library, making it look like a KeyError. I believe this could mislead developers, so it's better to let the original exception bubble up.

@d15ky
Copy link
Contributor Author

d15ky commented Apr 28, 2025

One thing to add @d15ky, Key Vault should always be case-insensitive:

Well, I’ve only enabled case-insensitivity, not the other way around. It was always case-sensitive after the last update (2.9.0+) — that’s what motivated me to make this PR in the first place. I’m fine if you suggest making it always case-insensitive.

@AndreuCodina
Copy link
Contributor

One thing to add @d15ky, Key Vault should always be case-insensitive:

Well, I’ve only enabled case-insensitivity, not the other way around. It was always case-sensitive after the last update (2.9.0+) — that’s what motivated me to make this PR in the first place. I’m fine if you suggest making it always case-insensitive.

Normally you only find information regarding the .NET provider, but this documentation is common, so I think it's the approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants