diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..b6a8cd2
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,18 @@
+{
+ "name": "C# (.NET 10)",
+ "image": "mcr.microsoft.com/dotnet/sdk:10.0",
+ "updateRemoteUserUID": true,
+ "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/${localWorkspaceFolderBasename},type=bind,Z",
+ "runArgs": [
+ "--userns=keep-id"
+ ],
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-dotnettools.csharp",
+ "ms-dotnettools.vscode-dotnet-runtime",
+ // "ms-dotnettools.csdevkit"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..bb400d4
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,389 @@
+root = true
+
+# All files
+[*]
+indent_style = space
+
+# Xml files
+[*.xml]
+indent_size = 2
+
+# Xml project files
+[*.{csproj,fsproj,vbproj,proj,slnx}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,config,nuspec}]
+indent_size = 2
+
+[*.json]
+indent_size = 2
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+tab_width = 4
+
+# New line preferences
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+[*.{cs,vb}]
+
+# Organize usings
+dotnet_separate_import_directive_groups = true
+dotnet_sort_system_directives_first = true
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_property = false:silent
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_collection_expression = true:warning
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+# Field preferences
+dotnet_style_readonly_field = true:warning
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:suggestion
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+#### C# Coding Conventions ####
+[*.cs]
+
+# var preferences
+csharp_style_var_elsewhere = false:silent
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_prefer_switch_expression = true:suggestion
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+csharp_prefer_static_anonymous_function = true:suggestion
+csharp_prefer_static_local_function = true:warning
+csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
+
+# Code-block preferences
+csharp_prefer_braces = true:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:suggestion
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_primary_constructors = true:warning
+csharp_style_prefer_top_level_statements = true:silent
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:silent
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+[*.{cs,vb}]
+
+# Naming rules
+
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
+dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
+dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
+
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
+
+dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
+dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
+dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.events_should_be_pascalcase.symbols = events
+dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
+dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
+dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
+dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
+dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
+dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
+dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
+
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = warning
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
+
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
+dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
+
+# Symbol specifications
+
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interfaces.required_modifiers =
+
+dotnet_naming_symbols.enums.applicable_kinds = enum
+dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.enums.required_modifiers =
+
+dotnet_naming_symbols.events.applicable_kinds = event
+dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.events.required_modifiers =
+
+dotnet_naming_symbols.methods.applicable_kinds = method
+dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.methods.required_modifiers =
+
+dotnet_naming_symbols.properties.applicable_kinds = property
+dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.properties.required_modifiers =
+
+dotnet_naming_symbols.public_fields.applicable_kinds = field
+dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_fields.required_modifiers =
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_fields.required_modifiers =
+
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+
+dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
+dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types_and_namespaces.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
+dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
+dotnet_naming_symbols.type_parameters.required_modifiers =
+
+dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.local_variables.applicable_kinds = local
+dotnet_naming_symbols.local_variables.applicable_accessibilities = local
+dotnet_naming_symbols.local_variables.required_modifiers =
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.applicable_accessibilities = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+
+dotnet_naming_symbols.parameters.applicable_kinds = parameter
+dotnet_naming_symbols.parameters.applicable_accessibilities = *
+dotnet_naming_symbols.parameters.required_modifiers =
+
+dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+dotnet_naming_symbols.local_functions.applicable_accessibilities = *
+dotnet_naming_symbols.local_functions.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascalcase.required_prefix =
+dotnet_naming_style.pascalcase.required_suffix =
+dotnet_naming_style.pascalcase.word_separator =
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+
+dotnet_naming_style.ipascalcase.required_prefix = I
+dotnet_naming_style.ipascalcase.required_suffix =
+dotnet_naming_style.ipascalcase.word_separator =
+dotnet_naming_style.ipascalcase.capitalization = pascal_case
+
+dotnet_naming_style.tpascalcase.required_prefix = T
+dotnet_naming_style.tpascalcase.required_suffix =
+dotnet_naming_style.tpascalcase.word_separator =
+dotnet_naming_style.tpascalcase.capitalization = pascal_case
+
+dotnet_naming_style._camelcase.required_prefix = _
+dotnet_naming_style._camelcase.required_suffix =
+dotnet_naming_style._camelcase.word_separator =
+dotnet_naming_style._camelcase.capitalization = camel_case
+
+dotnet_naming_style.camelcase.required_prefix =
+dotnet_naming_style.camelcase.required_suffix =
+dotnet_naming_style.camelcase.word_separator =
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_style.s_camelcase.required_prefix = s_
+dotnet_naming_style.s_camelcase.required_suffix =
+dotnet_naming_style.s_camelcase.word_separator =
+dotnet_naming_style.s_camelcase.capitalization = camel_case
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..5782b40
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,28 @@
+name: CI
+
+on:
+ push:
+ branches: [ master, main ]
+ pull_request:
+ branches: [ master, main ]
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x' # Uses latest stable .NET
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --configuration Release --no-restore
+
+ - name: Test
+ run: dotnet test test/test.csproj --configuration Release --no-build --verbosity normal
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
new file mode 100644
index 0000000..437d6be
--- /dev/null
+++ b/.github/workflows/format.yml
@@ -0,0 +1,22 @@
+name: Format Check
+
+on:
+ push:
+ branches: [ master, main ]
+ pull_request:
+ branches: [ master, main ]
+
+jobs:
+ format:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Format Check
+ run: dotnet format --verify-no-changes
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..d4d3e63
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,31 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*' # Trigger on tags like v4.0.0 or v4.0.0-rc.1
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Extract version from tag
+ id: version
+ run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
+
+ - name: Restore dependencies
+ run: dotnet restore Hjson/Hjson.csproj
+
+ - name: Pack Hjson
+ run: dotnet pack Hjson/Hjson.csproj --configuration Release --no-restore -o ./artifacts /p:Version=${{ steps.version.outputs.VERSION }}
+
+ - name: Push to NuGet
+ run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 35cabb5..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-language: csharp
-mono: 4.0.0
-dotnet: 2.0.0
-script:
- - ./build-core
- - ./build-mono
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..bc54960
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,10 @@
+{
+ "[csharp]": {
+ "editor.defaultFormatter": "ms-dotnettools.csharp",
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.csharp": "explicit"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Hjson.nuspec b/Hjson.nuspec
deleted file mode 100644
index ae5baee..0000000
--- a/Hjson.nuspec
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- Hjson
- $version$
- Hjson
- Christian Zangl
- Christian Zangl
- https://github.com/laktak/hjson-cs/blob/master/LICENSE
- http://hjson.org
- false
- Hjson, a user interface for JSON. Relaxed syntax, fewer mistakes, more comments. Supports .NET Core, .NET 4.x and Mono. For details go to http://hjson.org.
- Copyright Christian Zangl
- JSON comments config hjson parser serializer
-
-
-
-
-
-
-
-
diff --git a/Hjson.sln b/Hjson.sln
deleted file mode 100644
index 53ead1f..0000000
--- a/Hjson.sln
+++ /dev/null
@@ -1,22 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26114.2
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hjson", "Hjson\Hjson.csproj", "{FF9E2637-8BD3-4F8D-B563-D105B10D5354}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
-EndGlobal
diff --git a/Hjson.slnx b/Hjson.slnx
new file mode 100644
index 0000000..2e87ed8
--- /dev/null
+++ b/Hjson.slnx
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Hjson/BaseReader.cs b/Hjson/BaseReader.cs
index 352bf08..96e4220 100644
--- a/Hjson/BaseReader.cs
+++ b/Hjson/BaseReader.cs
@@ -1,287 +1,310 @@
using System;
-using System.Collections;
-using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Text;
-namespace Hjson
+namespace Hjson;
+
+internal abstract class BaseReader
{
- internal abstract class BaseReader
- {
- string buffer;
- TextReader r;
- StringBuilder sb=new StringBuilder();
- StringBuilder white=new StringBuilder();
- // peek could be removed since we now use a buffer
- List peek=new List();
+ readonly string buffer;
+ int pos;
+ readonly StringBuilder sb = new();
+ readonly StringBuilder white = new();
bool prevLf;
public int Line { get; private set; }
public int Column { get; private set; }
protected IJsonReader Reader { get; private set; }
- protected bool HasReader { get { return Reader!=null; } }
+ protected bool HasReader => Reader != null;
public bool ReadWsc { get; set; }
+ public BaseReader(string input, IJsonReader jsonReader)
+ {
+ ArgumentNullException.ThrowIfNull(input);
+ Reader = jsonReader;
+ buffer = input;
+ Reset();
+ }
+
public BaseReader(TextReader reader, IJsonReader jsonReader)
{
- if (reader==null) throw new ArgumentNullException("reader");
- // use a buffer so we can support reset
- this.Reader=jsonReader;
- buffer=reader.ReadToEnd();
- Reset();
+ ArgumentNullException.ThrowIfNull(reader);
+ Reader = jsonReader;
+ buffer = reader.ReadToEnd();
+ Reset();
}
public void Reset()
{
- Line=1;
- this.r=new StringReader(buffer);
- peek.Clear();
- white.Length=sb.Length=0;
- prevLf=false;
+ Line = 1;
+ pos = 0;
+ white.Length = sb.Length = 0;
+ prevLf = false;
}
- public int PeekChar(int idx=0)
+ public int PeekChar(int idx = 0)
{
- if (idx<0) throw new ArgumentOutOfRangeException();
- while (idx>=peek.Count)
- {
- int c=r.Read();
- if (c<0) return c;
- peek.Add(c);
- }
- return peek[idx];
+ if (idx < 0) throw new ArgumentOutOfRangeException(nameof(idx));
+ int p = pos + idx;
+ return (uint)p < (uint)buffer.Length ? buffer[p] : -1;
}
+ public bool RemainingContains(char c) => buffer.AsSpan(pos).Contains(c);
+
public virtual int SkipPeekChar()
{
- SkipWhite();
- return PeekChar();
+ SkipWhite();
+ return PeekChar();
}
public int ReadChar()
{
- int v;
- if (peek.Count>0)
- {
- // normally peek will only hold not more than one character so this should not matter for performance
- v=peek[0];
- peek.RemoveAt(0);
- }
- else v=r.Read();
-
- if (ReadWsc && v!='\r') white.Append((char)v);
-
- if (prevLf)
- {
- Line++;
- Column=0;
- prevLf=false;
- }
-
- if (v=='\n') prevLf=true;
- Column++;
-
- return v;
+ if ((uint)pos >= (uint)buffer.Length) return -1;
+
+ char v = buffer[pos++];
+
+ if (ReadWsc && v != '\r') white.Append(v);
+
+ if (prevLf)
+ {
+ Line++;
+ Column = 0;
+ prevLf = false;
+ }
+
+ if (v == '\n') prevLf = true;
+ Column++;
+
+ return v;
}
protected void ResetWhite()
{
- if (ReadWsc) white.Length=0;
+ if (ReadWsc) white.Length = 0;
}
protected virtual string GetWhite()
{
- if (!ReadWsc) throw new InvalidOperationException();
- return white.ToString();
+ if (!ReadWsc) throw new InvalidOperationException();
+ return white.ToString();
}
public static bool IsWhite(char c)
{
- return c==' ' || c=='\t' || c=='\r' || c=='\n';
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
public void SkipWhite()
{
- for (; ; )
- {
- if (IsWhite((char)PeekChar())) ReadChar();
- else break;
- }
+ while (IsWhite((char)PeekChar())) ReadChar();
}
- // It could return either long or double, depending on the parsed value.
+ // Returns either long or double, depending on the parsed value.
public JsonValue ReadNumericLiteral()
{
- int c, leadingZeros=0;
- double val=0;
- bool negative=false, testLeading=true;
-
- if (PeekChar()=='-')
- {
- negative=true;
- ReadChar();
- if (PeekChar()<0) throw ParseError("Invalid JSON numeric literal; extra negation");
- }
-
- for (int x=0; ; x++)
- {
- c=PeekChar();
- if (c<'0' || c>'9') break;
- if (testLeading)
+ int c, leadingZeros = 0;
+ bool testLeading = true;
+ Span numBuf = stackalloc char[64];
+ int numLen = 0;
+
+ if (PeekChar() == '-')
{
- if (c=='0') leadingZeros++;
- else testLeading = false;
+ numBuf[numLen++] = '-';
+ ReadChar();
+ if (PeekChar() < 0) throw ParseError("Invalid JSON numeric literal; extra negation");
}
- val=val*10+(c-'0');
- ReadChar();
- }
- if (testLeading) leadingZeros--; // single 0 is allowed
- if (leadingZeros>0) throw ParseError("leading multiple zeros are not allowed");
-
- // fraction
- if (PeekChar()=='.')
- {
- int fdigits=0;
- double frac=0;
- ReadChar();
- if (PeekChar()<0) throw ParseError("Invalid JSON numeric literal; extra dot");
- double d=10;
+
for (; ; )
{
- c=PeekChar();
- if (c<'0' || '9' '9') break;
+ if (testLeading)
+ {
+ if (c == '0') leadingZeros++;
+ else testLeading = false;
+ }
+ numBuf[numLen++] = (char)c;
+ ReadChar();
}
- if (fdigits==0) throw ParseError("Invalid JSON numeric literal; extra dot");
- val+=frac;
- }
+ if (testLeading) leadingZeros--; // single 0 is allowed
+ if (leadingZeros > 0) throw ParseError("leading multiple zeros are not allowed");
- c=PeekChar();
- if (c=='e' || c=='E')
- {
- // exponent
- int exp=0, expSign=1;
+ bool hasFracOrExp = false;
- ReadChar();
- if (PeekChar()<0) throw new ArgumentException("Invalid JSON numeric literal; incomplete exponent");
+ // fraction
+ if (PeekChar() == '.')
+ {
+ hasFracOrExp = true;
+ int fdigits = 0;
+ numBuf[numLen++] = '.';
+ ReadChar();
+ if (PeekChar() < 0) throw ParseError("Invalid JSON numeric literal; extra dot");
+ for (; ; )
+ {
+ c = PeekChar();
+ if (c < '0' || '9' < c) break;
+ numBuf[numLen++] = (char)c;
+ ReadChar();
+ fdigits++;
+ }
+ if (fdigits == 0) throw ParseError("Invalid JSON numeric literal; extra dot");
+ }
- c=PeekChar();
- if (c=='-')
+ c = PeekChar();
+ if (c == 'e' || c == 'E')
{
- ReadChar();
- expSign=-1;
+ hasFracOrExp = true;
+ numBuf[numLen++] = (char)c;
+ ReadChar();
+ if (PeekChar() < 0) throw new ArgumentException("Invalid JSON numeric literal; incomplete exponent");
+
+ c = PeekChar();
+ if (c == '-')
+ {
+ numBuf[numLen++] = '-';
+ ReadChar();
+ }
+ else if (c == '+')
+ {
+ numBuf[numLen++] = '+';
+ ReadChar();
+ }
+
+ if (PeekChar() < 0) throw ParseError("Invalid JSON numeric literal; incomplete exponent");
+
+ for (; ; )
+ {
+ c = PeekChar();
+ if (c < '0' || c > '9') break;
+ numBuf[numLen++] = (char)c;
+ ReadChar();
+ }
}
- else if (c=='+') ReadChar();
- if (PeekChar()<0) throw ParseError("Invalid JSON numeric literal; incomplete exponent");
+ var numSpan = numBuf[..numLen];
- for (; ; )
+ // Try parsing as long first to preserve precision for large integers
+ if (!hasFracOrExp && long.TryParse(numSpan, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out long lval))
{
- c=PeekChar();
- if (c<'0' || c>'9') break;
- exp=exp*10+(c-'0');
- ReadChar();
+ // Preserve negative zero as double (-0 as long is just 0)
+ if (lval == 0 && numSpan[0] == '-') return -0.0;
+ return lval;
}
- if (exp!=0)
- val*=Math.Pow(10, exp*expSign);
- }
+ double val = double.Parse(numSpan, NumberStyles.Float, NumberFormatInfo.InvariantInfo);
+ if (val == 0.0 && double.IsNegative(val)) return -0.0;
- if (negative) val*=-1;
- long lval=(long)val;
- if (lval==val) return lval;
- else return val;
+ return val;
}
public string ReadStringLiteral(Func allowML)
{
- // callers make sure that (exitCh == '"' || exitCh == "'")
-
- int exitCh=ReadChar();
- sb.Length=0;
- for (; ; )
- {
- int c=ReadChar();
- if (c<0) throw ParseError("JSON string is not closed");
- if (c==exitCh)
+ // callers make sure that (exitCh == '"' || exitCh == "'")
+ int exitCh = ReadChar();
+
+ // Check for multiline ''' string
+ if (allowML != null && exitCh == '\''
+ && (uint)pos < (uint)buffer.Length && buffer[pos] == '\''
+ && (uint)(pos + 1) < (uint)buffer.Length && buffer[pos + 1] == '\'')
{
- if (allowML!=null && exitCh=='\'' && PeekChar()=='\'' && sb.Length==0)
- {
- // ''' indicates a multiline string
- ReadChar();
+ ReadChar(); ReadChar();
return allowML();
- }
- else return sb.ToString();
}
- else if (c=='\n' || c=='\r')
+
+ // Scan to find closing quote and detect escapes
+ int start = pos;
+ bool hasEscapes = false;
+ int i = start;
+ int bufLen = buffer.Length;
+ while (i < bufLen)
{
- throw ParseError("Bad string containing newline");
+ char c = buffer[i];
+ if (c == exitCh) break;
+ if (c == '\\') { hasEscapes = true; i++; } // skip escaped char
+ else if (c == '\n' || c == '\r') break;
+ i++;
}
- else if (c!='\\')
+ if (i >= bufLen) throw ParseError("JSON string is not closed");
+ if (buffer[i] != exitCh) throw ParseError("Bad string containing newline");
+
+ int end = i; // position of closing quote
+
+ string result;
+ if (!hasEscapes)
{
- sb.Append((char)c);
- continue;
+ result = buffer.Substring(start, end - start);
}
-
- // escaped expression
- c=ReadChar();
- if (c<0)
- throw ParseError("Invalid JSON string literal; incomplete escape sequence");
- switch (c)
+ else
{
- case '"':
- case '\'':
- case '\\':
- case '/': sb.Append((char)c); break;
- case 'b': sb.Append('\x8'); break;
- case 'f': sb.Append('\f'); break;
- case 'n': sb.Append('\n'); break;
- case 'r': sb.Append('\r'); break;
- case 't': sb.Append('\t'); break;
- case 'u':
- ushort cp=0;
- for (int i=0; i<4; i++)
+ sb.Length = 0;
+ for (i = start; i < end; i++)
{
- cp <<= 4;
- if ((c=ReadChar())<0)
- throw ParseError("Incomplete unicode character escape literal");
- if (c>='0' && c<='9') cp+=(ushort)(c-'0');
- else if (c>='A' && c<='F') cp+=(ushort)(c-'A'+10);
- else if (c>='a' && c<='f') cp+=(ushort)(c-'a'+10);
- else throw ParseError("Bad \\u char "+(char)c);
+ char c = buffer[i];
+ if (c != '\\') { sb.Append(c); continue; }
+
+ if (++i >= end)
+ throw ParseError("Invalid JSON string literal; incomplete escape sequence");
+ c = buffer[i];
+ switch (c)
+ {
+ case '"':
+ case '\'':
+ case '\\':
+ case '/': sb.Append(c); break;
+ case 'b': sb.Append('\x8'); break;
+ case 'f': sb.Append('\f'); break;
+ case 'n': sb.Append('\n'); break;
+ case 'r': sb.Append('\r'); break;
+ case 't': sb.Append('\t'); break;
+ case 'u':
+ ushort cp = 0;
+ for (int j = 0; j < 4; j++)
+ {
+ if (++i >= end)
+ throw ParseError("Incomplete unicode character escape literal");
+ cp <<= 4;
+ c = buffer[i];
+ if (c >= '0' && c <= '9') cp += (ushort)(c - '0');
+ else if (c >= 'A' && c <= 'F') cp += (ushort)(c - 'A' + 10);
+ else if (c >= 'a' && c <= 'f') cp += (ushort)(c - 'a' + 10);
+ else throw ParseError("Bad \\u char " + c);
+ }
+ sb.Append((char)cp);
+ break;
+ default:
+ throw ParseError("Invalid JSON string literal; unexpected escape character");
+ }
}
- sb.Append((char)cp);
- break;
- default:
- throw ParseError("Invalid JSON string literal; unexpected escape character");
+ result = sb.ToString();
}
- }
+
+ // Update reader state (no newlines in valid strings)
+ if (ReadWsc) white.Append(buffer, start, end + 1 - start);
+ pos = end + 1;
+ Column += end - start + 1;
+
+ return result;
}
public void Expect(char expected)
{
- int c;
- if ((c=ReadChar())!=expected)
- throw ParseError(String.Format("Expected '{0}', got '{1}'", expected, (char)c));
+ int c;
+ if ((c = ReadChar()) != expected)
+ throw ParseError($"Expected '{expected}', got '{(char)c}'");
}
public void Expect(string expected)
{
- for (int i=0; i
- 3.0.1
- netstandard2.0
+ net10.0
true
Hjson
+
+
Hjson
- false
- false
- false
- false
- false
- false
+ 4.0.0
+ Christian Zangl;V
+ Hjson, a user interface for JSON. Relaxed syntax, fewer mistakes, more comments. For details go to http://hjson.github.io.
+ Copyright Christian Zangl
+ JSON;comments;config;hjson;parser;serializer
+ MIT
+ https://hjson.github.io
+ https://github.com/hjson/hjson-cs
+ README.md
+
+
+
+
diff --git a/Hjson/HjsonAttributes.cs b/Hjson/HjsonAttributes.cs
new file mode 100644
index 0000000..dbc23f1
--- /dev/null
+++ b/Hjson/HjsonAttributes.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Hjson;
+
+/// Specifies the property name used in Hjson serialization.
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public sealed class HjsonPropertyNameAttribute(string name) : Attribute
+{
+ /// Gets the name of the property.
+ public string Name { get; } = name;
+}
+
+/// Indicates that a property should be ignored during Hjson serialization.
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public sealed class HjsonIgnoreAttribute : Attribute;
+
+/// Indicates that a non-public property or field should be included during Hjson serialization.
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public sealed class HjsonIncludeAttribute : Attribute;
+
+/// Specifies a comment to be written above the property in Hjson output.
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public sealed class HjsonCommentAttribute(string comment) : Attribute
+{
+ /// Gets the comment text.
+ public string Comment { get; } = comment;
+}
\ No newline at end of file
diff --git a/Hjson/HjsonConvert.cs b/Hjson/HjsonConvert.cs
new file mode 100644
index 0000000..e7b445e
--- /dev/null
+++ b/Hjson/HjsonConvert.cs
@@ -0,0 +1,325 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json.Serialization;
+
+namespace Hjson;
+
+/// Provides methods for serializing and deserializing objects to/from Hjson.
+///
+/// Supports Hjson-specific attributes (,
+/// , ,
+/// ) with fallback to System.Text.Json attributes
+/// (, ,
+/// ).
+///
+public static class HjsonConvert
+{
+ /// Serializes an object to an Hjson string.
+ public static string Serialize(object obj, HjsonOptions options = null)
+ {
+ var jsonValue = ToJsonValue(obj);
+ if (jsonValue == null) return "null";
+
+ using var sw = new StringWriter();
+ bool hasComments = HasAnyComments(jsonValue);
+ var opts = options ?? new HjsonOptions();
+ if (hasComments) opts.KeepWsc = true;
+ HjsonValue.Save(jsonValue, sw, opts);
+ return sw.ToString();
+ }
+
+ /// Deserializes an Hjson string to an object of type .
+ public static T Deserialize(string hjson, HjsonOptions options = null)
+ {
+ var jsonValue = options != null
+ ? HjsonValue.Parse(hjson, options)
+ : HjsonValue.Parse(hjson);
+ return (T)FromJsonValue(jsonValue, typeof(T));
+ }
+
+ // ── Serialization (Object → JsonValue) ──────────────────────────
+
+ static JsonValue ToJsonValue(object obj)
+ {
+ if (obj == null) return null;
+
+ var type = obj.GetType();
+
+ // Primitives handled by implicit operators
+ if (obj is bool b) return b;
+ if (obj is string s) return s;
+ if (obj is char c) return new string(c, 1);
+ if (obj is int i) return i;
+ if (obj is long l) return l;
+ if (obj is double d) return d;
+ if (obj is float f) return f;
+ if (obj is decimal dec) return dec;
+ if (obj is byte by) return by;
+ if (obj is short sh) return sh;
+
+ // Enum → string
+ if (type.IsEnum) return obj.ToString();
+
+ // Note: Nullable is already unboxed to T by the CLR when boxed
+
+ // Dictionary
+ if (obj is IDictionary dict && type.IsGenericType)
+ {
+ var keyType = type.GetGenericArguments()[0];
+ if (keyType == typeof(string))
+ {
+ var jsonObj = new JsonObject();
+ foreach (DictionaryEntry entry in dict)
+ jsonObj.Add((string)entry.Key, ToJsonValue(entry.Value));
+ return jsonObj;
+ }
+ }
+
+ // Array / List / IEnumerable (but not string)
+ if (obj is IEnumerable enumerable)
+ {
+ var jsonArr = new JsonArray();
+ foreach (var item in enumerable)
+ jsonArr.Add(ToJsonValue(item));
+ return jsonArr;
+ }
+
+ // Complex object → reflect
+ return ObjectToJsonValue(obj, type);
+ }
+
+ static JsonValue ObjectToJsonValue(object obj, Type type)
+ {
+ var members = GetMembers(type);
+ bool hasComments = members.Any(m => m.Comment != null);
+
+ WscJsonObject wscObj = null;
+ JsonObject jsonObj;
+
+ if (hasComments)
+ {
+ wscObj = new WscJsonObject { RootBraces = true };
+ jsonObj = wscObj;
+ wscObj.Comments[""] = "";
+ }
+ else
+ {
+ jsonObj = new JsonObject();
+ }
+
+ foreach (var member in members)
+ {
+ var value = member.GetValue(obj);
+ var jsonValue = ToJsonValue(value);
+ jsonObj.Add(member.HjsonName, jsonValue);
+
+ if (wscObj != null)
+ {
+ wscObj.Order.Add(member.HjsonName);
+ wscObj.Comments[member.HjsonName] = member.Comment != null
+ ? "\n" + FormatComment(member.Comment)
+ : "";
+ }
+ }
+
+ return jsonObj;
+ }
+
+ static string FormatComment(string comment)
+ {
+ var lines = comment.Replace("\r\n", "\n").Split('\n');
+ return string.Join("\n", lines.Select(line => "# " + line));
+ }
+
+ static bool HasAnyComments(JsonValue value) => value is WscJsonObject;
+
+ // ── Deserialization (JsonValue → Object) ─────────────────────────
+
+ static object FromJsonValue(JsonValue value, Type targetType)
+ {
+ if (value == null)
+ {
+ if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null)
+ return Activator.CreateInstance(targetType);
+ return null;
+ }
+
+ // Unwrap Nullable
+ var underlying = Nullable.GetUnderlyingType(targetType);
+ if (underlying != null) targetType = underlying;
+
+ // Primitives
+ if (targetType == typeof(bool)) return (bool)value;
+ if (targetType == typeof(string)) return (string)value;
+ if (targetType == typeof(char))
+ {
+ var str = (string)value;
+ return str.Length > 0 ? str[0] : default;
+ }
+ if (targetType == typeof(int)) return (int)value;
+ if (targetType == typeof(long)) return (long)value;
+ if (targetType == typeof(double)) return (double)value;
+ if (targetType == typeof(float)) return (float)value;
+ if (targetType == typeof(decimal)) return (decimal)value;
+ if (targetType == typeof(byte)) return (byte)value;
+ if (targetType == typeof(short)) return (short)value;
+
+ // Enum
+ if (targetType.IsEnum)
+ {
+ if (value.JsonType == JsonType.String)
+ return Enum.Parse(targetType, (string)value, ignoreCase: true);
+ if (value.JsonType == JsonType.Number)
+ return Enum.ToObject(targetType, (int)value);
+ }
+
+ // JsonValue passthrough — if someone wants the raw value
+ if (targetType == typeof(JsonValue) || targetType == typeof(JsonObject) ||
+ targetType == typeof(JsonArray) || targetType == typeof(JsonPrimitive))
+ return value;
+
+ // Array
+ if (targetType.IsArray)
+ {
+ var elementType = targetType.GetElementType()!;
+ var arr = value.Count > 0 ? new object[value.Count] : [];
+ for (int i = 0; i < value.Count; i++)
+ arr[i] = FromJsonValue(value[i], elementType);
+ var typed = Array.CreateInstance(elementType, arr.Length);
+ Array.Copy(arr, typed, arr.Length);
+ return typed;
+ }
+
+ // List / IList / IEnumerable / ICollection
+ if (targetType.IsGenericType)
+ {
+ var genDef = targetType.GetGenericTypeDefinition();
+ var genArgs = targetType.GetGenericArguments();
+
+ // Dictionary
+ if ((genDef == typeof(Dictionary<,>) || genDef == typeof(IDictionary<,>)) &&
+ genArgs[0] == typeof(string))
+ {
+ var dictType = genDef == typeof(IDictionary<,>)
+ ? typeof(Dictionary<,>).MakeGenericType(genArgs)
+ : targetType;
+ var dict = (IDictionary)Activator.CreateInstance(dictType)!;
+ if (value is JsonObject obj)
+ {
+ foreach (var key in obj.Keys)
+ dict[key] = FromJsonValue(obj[key], genArgs[1]);
+ }
+ return dict;
+ }
+
+ // List, IList, ICollection, IEnumerable
+ if (genDef == typeof(List<>) || genDef == typeof(IList<>) ||
+ genDef == typeof(ICollection<>) || genDef == typeof(IEnumerable<>))
+ {
+ var listType = (genDef == typeof(List<>))
+ ? targetType
+ : typeof(List<>).MakeGenericType(genArgs);
+ var list = (IList)Activator.CreateInstance(listType)!;
+ for (int i = 0; i < value.Count; i++)
+ list.Add(FromJsonValue(value[i], genArgs[0]));
+ return list;
+ }
+ }
+
+ // Complex object
+ if (value is JsonObject jsonObj)
+ return ObjectFromJsonValue(jsonObj, targetType);
+
+ throw new InvalidOperationException($"Cannot convert {value.JsonType} to {targetType.Name}");
+ }
+
+ static object ObjectFromJsonValue(JsonObject jsonObj, Type type)
+ {
+ var instance = Activator.CreateInstance(type)!;
+ var members = GetMembers(type);
+
+ foreach (var member in members)
+ {
+ if (!jsonObj.ContainsKey(member.HjsonName)) continue;
+ var jsonValue = jsonObj[member.HjsonName];
+ var converted = FromJsonValue(jsonValue, member.MemberType);
+ member.SetValue(instance, converted);
+ }
+
+ return instance;
+ }
+
+ // ── Member reflection ────────────────────────────────────────────
+
+ sealed class MemberDescriptor
+ {
+ public string HjsonName;
+ public string Comment;
+ public Type MemberType;
+ public Func