[GH-ISSUE #49] libcorn passes all environment variables to serde as strings, regardless of the underlying type #17

Open
opened 2026-05-22 22:05:10 +01:00 by JakeStanger · 3 comments
Owner

Originally created by @tarka on GitHub (Dec 22, 2025).
Original GitHub issue: https://github.com/corn-config/corn/issues/49

Having native environment variable injection is a powerful feature as it allows 12-factor type configuration, however it doesn't play well with non-string types (AFAICS; happy to be proven wrong).

e.g. If you have the following config definitions (based on a real use-case):

#[derive(Deserialize)]
pub struct Insecure {
    pub port: u16,
    pub redirect: bool,
}

#[derive(Deserialize)]
pub struct TlsConfig {
    pub port: u16,
}

#[derive(Deserialize)]
pub struct Config {
    pub insecure: Insecure,
    pub tls: TlsConfig,
}

And this config file:

let {
    $env_INSECURE_PORT = 80
    $env_INSECURE_REDIRECT = true
    $env_TLS_PORT = 443
} in {
    insecure = {
        port = $env_INSECURE_PORT
        redirect = $env_INSECURE_REDIRECT
    }
    tls = {
        port = $env_TLS_PORT
    }
}

If you deserialise with no environment variables set then it works fine. However setting any of the env-vars will result in DeserializationError("Expected integer (u16), found 'String(\"8080\")'" (or DeserializationError("Expected boolean, found 'String(\"false\")'")).

I'm not sure if there's any simple workaround for this? It looks like the only way is to make any variables that may be provided from the environment into strings an have serde parse them into the appropriate type (with serde-with or similar). However the user could potentially supply any variable, so everything would end up being a string.

Originally created by @tarka on GitHub (Dec 22, 2025). Original GitHub issue: https://github.com/corn-config/corn/issues/49 Having native environment variable injection is a powerful feature as it allows 12-factor type configuration, however it doesn't play well with non-string types (AFAICS; happy to be proven wrong). e.g. If you have the following config definitions (based on a real use-case): ```rust #[derive(Deserialize)] pub struct Insecure { pub port: u16, pub redirect: bool, } #[derive(Deserialize)] pub struct TlsConfig { pub port: u16, } #[derive(Deserialize)] pub struct Config { pub insecure: Insecure, pub tls: TlsConfig, } ``` And this config file: ```corn let { $env_INSECURE_PORT = 80 $env_INSECURE_REDIRECT = true $env_TLS_PORT = 443 } in { insecure = { port = $env_INSECURE_PORT redirect = $env_INSECURE_REDIRECT } tls = { port = $env_TLS_PORT } } ``` If you deserialise with no environment variables set then it works fine. However setting any of the env-vars will result in `DeserializationError("Expected integer (u16), found 'String(\"8080\")'"` (or `DeserializationError("Expected boolean, found 'String(\"false\")'")`). I'm not sure if there's any simple workaround for this? It looks like the only way is to make any variables that may be provided from the environment into strings an have serde parse them into the appropriate type (with `serde-with` or similar). However the user could potentially supply any variable, so everything would end up being a string.
Author
Owner

@tarka commented on GitHub (Dec 22, 2025):

There's a full test-case here if it helps: https://github.com/tarka/corn-test/blob/main/src/lib.rs

<!-- gh-comment-id:3680106233 --> @tarka commented on GitHub (Dec 22, 2025): There's a full test-case here if it helps: https://github.com/tarka/corn-test/blob/main/src/lib.rs
Author
Owner

@JakeStanger commented on GitHub (Dec 22, 2025):

Interesting, this is definitely an oversight.

In my head env vars would always be strings because that's how the OS treats them, but the specification doesn't actually state that (and even gives an integer-based example) and this is definitely a valid usecase.

Currently you will have to treat all env vars as strings and work around that the best you can, either with serde or by parsing in code. I am definitely open to amending that though.

<!-- gh-comment-id:3682932085 --> @JakeStanger commented on GitHub (Dec 22, 2025): Interesting, this is definitely an oversight. In my head env vars would always be strings because that's how the OS treats them, but the specification doesn't actually state that (and even gives an integer-based example) and this is definitely a valid usecase. Currently you will have to treat all env vars as strings and work around that the best you can, either with serde or by parsing in code. I am definitely open to amending that though.
Author
Owner

@tarka commented on GitHub (Dec 23, 2025):

One quick workaround for this would be to specify that non-string $env inputs should have a default. This would allow you to use the inputs lookup to retrieve the expected type and attempt a coercion. This would only really work with primitive types though (int, float, bool). But that would probably cover most normal cases. I've hacked together a quick proof-of-concept here and it works for the example above.

Obviously it would be much better to retrieve the type from the underlying deserialisation target.

<!-- gh-comment-id:3684751241 --> @tarka commented on GitHub (Dec 23, 2025): One quick workaround for this would be to specify that non-string $env inputs should have a default. This would allow you to use the inputs lookup to retrieve the expected type and attempt a coercion. This would only really work with primitive types though (int, float, bool). But that would probably cover most normal cases. I've hacked together a quick proof-of-concept [here](https://github.com/tarka/corn/commit/8341d3b704d912d434193b9f98cc83f0392d6732) and it works for the example above. Obviously it would be much better to retrieve the type from the underlying deserialisation target.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
corn-config/corn#17
No description provided.