Skip to content

Support jsonc format #457

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 1 commit into
base: main
Choose a base branch
from
Open

Support jsonc format #457

wants to merge 1 commit into from

Conversation

up9cloud
Copy link
Contributor

I did a new commit, see #452

Copy link
Member

@matthiasbeyer matthiasbeyer left a comment

Choose a reason for hiding this comment

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

I have some remarks. Also, the two commits could be squashed into one commit,... as well as further fixups of course!

Either way thanks for your contribution!

@@ -52,7 +52,7 @@ fn test_file() {
assert_eq!(s.place.telephone, None);
assert_eq!(s.elements.len(), 10);
assert_eq!(s.elements[3], "4".to_string());
if cfg!(feature = "preserve_order") {
if cfg!(feature = "TODO: preserve_order") {
Copy link
Member

Choose a reason for hiding this comment

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

Why this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well, that's because jsonc-parser doesn't support preserve_order. need time to contribute that

Comment on lines +91 to +93
#[cfg(all(feature = "jsonc", feature = "json"))]
formats.insert(FileFormat::Jsonc, vec!["jsonc"]);

#[cfg(all(feature = "jsonc", not(feature = "json")))]
formats.insert(FileFormat::Jsonc, vec!["jsonc", "json"]);
Copy link
Member

Choose a reason for hiding this comment

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

Do I understand correctly that the jsonc parser also parses json even if that feature is disabled?

I think that's counter-intuitive and we should only parse jsonc with the jsonc parser.

Copy link
Contributor Author

@up9cloud up9cloud Sep 20, 2023

Choose a reason for hiding this comment

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

well, there are 2 reasons i added that.

  1. the file extension for jsonc is actually .json, not .jsonc, so the real question is that when i enable the feature jsonc it's about the format or the file extension?
  2. for the common usage, people would choose only one of them, not both. and I believe the jsonc format is more common for config files than the json format.

@coveralls
Copy link

coveralls commented Aug 10, 2025

Pull Request Test Coverage Report for Build 16902150946

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.07%) to 65.345%

Totals Coverage Status
Change from base Build 16806457660: 0.07%
Covered Lines: 956
Relevant Lines: 1463

💛 - Coveralls

@up9cloud
Copy link
Contributor Author

Well, I merged the upstream and fixed the test cases.
However, I believe forcing lowercase mapping is an absolutely broken concept, and I don’t understand why after two years, this case’s behavior has changed to: https://github.com/up9cloud/config-rs/blob/master/tests/testsuite/file_jsonc.rs#L169.

I’m certain that when I submitted the pull request back then, all test cases passed.

This behavior should follow one clear approach, rather than just forcing lowercase:

  • Flexible: env.BAR, env.Bar, and env.bar should all map to enum Settings { Bar(String) }.
  • Strict: env.BAR should map only to BAR, env.bar should only map to bar, etc

@polarathene
Copy link
Collaborator

polarathene commented Aug 11, 2025

I’m certain that when I submitted the pull request back then, all test cases passed.

The feedback was back in 2023. You later pushed some commits in Feb 2024 and the PR was silent until recently. It's not a surprise that tests could fail given a new major zero ver release was made which permits breaking changes.


Regarding feedback with the jsonc feature concern for the .json extension, if it doesn't make sense to have other format features handle .json when jsonc is enabled, perhaps this could be detected as a conflict via build.rs? (unless Cargo.toml can express this, doesn't seem so)

I have seen crates like cudarc handle it in build.rs, so something like this?:

#[cfg(all(feature = "json", feature = "jsonc"))]
panic!("Both `json` and `jsonc` features are active, only one feature for parsing `.json` files is permitted");

Regarding src/file/format/jsonc.rs, what are your thoughts about this proposed common method for serde supported formats? As the jsonc-parser crate offers a parse_to_serde_value() method that returns a serde_json::Value, it should simplify the file in this PR to just:

use std::error::Error;

use crate::format;
use crate::map::Map;
use crate::value::Value;

pub fn parse(
    uri: Option<&String>,
    text: &str,
) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
    // Parse a JSON/JSONC input from the provided text
    let value = format::from_parsed_value(uri, jsonc_parser::parse_to_serde_value(text)?);
    format::extract_root_table(uri, value)
}

extract_root_table() is already merged so formats could skip this portion of common logic:

config-rs/src/format.rs

Lines 27 to 46 in 07f13ff

// Have a proper error fire if the root of a file is ever not a Table
pub(crate) fn extract_root_table(
uri: Option<&String>,
value: Value,
) -> Result<Map<String, Value>, Box<dyn Error + Send + Sync>> {
match value.kind {
ValueKind::Table(map) => Ok(map),
ValueKind::Nil => Err(Unexpected::Unit),
ValueKind::Array(_value) => Err(Unexpected::Seq),
ValueKind::Boolean(value) => Err(Unexpected::Bool(value)),
ValueKind::I64(value) => Err(Unexpected::I64(value)),
ValueKind::I128(value) => Err(Unexpected::I128(value)),
ValueKind::U64(value) => Err(Unexpected::U64(value)),
ValueKind::U128(value) => Err(Unexpected::U128(value)),
ValueKind::Float(value) => Err(Unexpected::Float(value)),
ValueKind::String(value) => Err(Unexpected::Str(value)),
}
.map_err(|err| ConfigError::invalid_root(uri, err))
.map_err(|err| Box::new(err) as Box<dyn Error + Send + Sync>)
}

So that should already simplify your parse() method for jsonc, but as my PR introducing the from_parsed_value() method has been recently questioned if it's worth merging with it's current approach, this PR would presently need to keep your from_jsonc_value() method.

The following should work for now at least:

-    let value = format::from_parsed_value(uri, jsonc_parser::parse_to_serde_value(text)?);
+    let value = from_jsonc_value(uri, jsonc_parser::parse_to_value(text, &Default::default())?);
    format::extract_root_table(uri, value)

Should #472 get merged, then from_jsonc_value() can also be dropped in favor of using format::from_parsed_value().


EDIT: Oh just noticed that the return type of the result is the value wrapped in an option, which unlike serde-json will result in "" returning None rather than an empty string. Going with the fallback you had instead:

-    let value = format::from_parsed_value(uri, jsonc_parser::parse_to_serde_value(text)?);
+    let parsed = jsonc_parser::parse_to_value(text, &Default::default())?;
+    let value = from_jsonc_value(uri, parsed.unwrap_or(ValueKind::Nil));
    format::extract_root_table(uri, value)

I'll apply that as a review suggestion 😅

@epage
Copy link
Contributor

epage commented Aug 11, 2025

Please rework your commits as meaningful, atomic commits to be merged.

@up9cloud
Copy link
Contributor Author

@epage, @polarathene all green now :D.

The .json vs .jsonc issue comes down to the project’s philosophy: should it aim to be flexible or strict?

If goes for flexible, the current behavior makes sense, because in practice, almost no one actually uses the .jsonc extension. People generally just want .json files that can contain comments.
If goes for strict, then only .jsonc should be allowed. Later, could consider adding a feature to configure which file extensions are parsed by which parser.

For example, from the earlier point about case insensitive handling and forcing lowercase mapping, these are closely tied to the overall design philosophy. Under a strict approach, case insensitive handling should be an optional feature rather than the default, and forcing lowercase mapping should be avoided - upper should map to upper, and lower should be lower.

@epage
Copy link
Contributor

epage commented Aug 12, 2025

From https://jsonc.org/

The recommended file extension for JSONC documents is .jsonc.

The extension .json should be avoided, but is supported if a mode line is present at the start of the file:

imo comments inside of json is no longer json. We should be properly validating the json we parse.

@@ -126,8 +126,9 @@ json = ["serde_json"]
yaml = ["yaml-rust2"]
ini = ["rust-ini"]
json5 = ["json5_rs", "serde/derive"]
jsonc = ["jsonc-parser"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
jsonc = ["jsonc-parser"]
jsonc = ["dep:jsonc-parser"]

#676 is tracking fixing the rest.

Comment on lines +255 to +267

#[test]
fn test_nothing() {
let res = Config::builder()
.add_source(File::from_str("", FileFormat::Ini))
.build();
assert!(res.is_ok());
let c = res.unwrap();
assert_data_eq!(
format!("{:?}", c),
str!("Config { defaults: {}, overrides: {}, sources: [], cache: Value { origin: None, kind: Table({}) } }")
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are these test_nothings added? if they are indirectly related (you wanted a test_nothing for jsonc), then please add these in their own commit before this one

assert!(res.is_ok());
let c = res.unwrap();
assert_data_eq!(
format!("{:?}", c),
Copy link
Contributor

Choose a reason for hiding this comment

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

c.to_debug()

Comment on lines +100 to +108
assert!(res.is_err());
let err = res.unwrap_err().to_string();
let expected_prefix =
"Expected colon after the string or word in object property on line 4 column 1 in ";
assert!(
err.starts_with(expected_prefix),
"Error message does not start with expected prefix. Got: {}",
err
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we switch this to snapshot testing (assert_data_eq!) like our other formats?

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

Successfully merging this pull request may close these issues.

5 participants