├── .editorconfig ├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── Directory.Build.props ├── LICENSE ├── README.md ├── SystemTextJson.FluentApi.sln ├── samples └── SystemTextJson.FluentApi.QuickStart │ ├── Program.cs │ └── SystemTextJson.FluentApi.QuickStart.csproj ├── src └── SystemTextJson.FluentApi │ ├── CustomEnumConverter.cs │ ├── CustomizableJsonStringEnumConverter.cs │ ├── EntityTypeBuilder.cs │ ├── EntityTypeBuilderExtensions.cs │ ├── IEntityTypeBuilder.cs │ ├── IHaveChangedProperties.cs │ ├── IMemberPropertyBuilder.cs │ ├── IPropertyBuilder.cs │ ├── InlineArrayJsonConverter.cs │ ├── JsonModelBuilder.cs │ ├── JsonPropertyInfoExtensions.cs │ ├── JsonSerializerOptionsExtensions.cs │ ├── JsonTypeInfoResolverExtensions.cs │ ├── MemberPropertyBuilder.cs │ ├── ObjectSerializer.cs │ ├── PropertyBuilderExtensions.cs │ ├── SerializationHelpers.cs │ ├── SystemTextJson.FluentApi.csproj │ ├── ValueTupleJsonConverter.cs │ └── VirtualPropertyBuilder.cs └── tests └── SystemTextJson.FluentApi.Tests ├── CustomizableJsonStringEnumConverterTests.cs ├── EntityTypeBuilderTests.cs ├── InlineArrayJsonConverterTests.cs ├── JsonAsserts.cs ├── JsonModelBuilderTests.cs ├── PropertyBuilderTests.cs ├── SystemTextJson.FluentApi.Tests.csproj └── ValueTupleJsonConverterTests.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | # from https://github.com/dotnet/roslyn/blob/master/.editorconfig 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 10 | 11 | # Code files 12 | [*.cs] 13 | indent_size = 4 14 | insert_final_newline = true 15 | charset = utf-8 16 | max_line_length = 120 17 | 18 | # Xml project files 19 | [*.csproj] 20 | indent_size = 2 21 | charset = utf-8 22 | 23 | # Xml config files 24 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 25 | indent_size = 2 26 | 27 | # JSON files 28 | [*.json] 29 | indent_size = 2 30 | 31 | [*.{sh}] 32 | end_of_line = lf 33 | indent_size = 2 34 | 35 | # Dotnet code style settings: 36 | [*.{cs,vb}] 37 | # Sort using and Import directives with System.* appearing first 38 | dotnet_sort_system_directives_first = true 39 | # Avoid "this." and "Me." if not necessary 40 | dotnet_style_qualification_for_field = false:suggestion 41 | dotnet_style_qualification_for_property = false:suggestion 42 | dotnet_style_qualification_for_method = false:suggestion 43 | dotnet_style_qualification_for_event = false:suggestion 44 | 45 | # Use language keywords instead of framework type names for type references 46 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 47 | dotnet_style_predefined_type_for_member_access = true:suggestion 48 | 49 | # Suggest more modern language features when available 50 | dotnet_style_object_initializer = true:suggestion 51 | dotnet_style_collection_initializer = true:suggestion 52 | dotnet_style_coalesce_expression = true:suggestion 53 | dotnet_style_null_propagation = true:suggestion 54 | dotnet_style_explicit_tuple_names = true:suggestion 55 | 56 | # Non-private static fields are PascalCase 57 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 58 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 59 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 60 | 61 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 62 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected 63 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 64 | 65 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 66 | 67 | # Constants are PascalCase 68 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 69 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 70 | dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style 71 | 72 | dotnet_naming_symbols.constants.applicable_kinds = field, local 73 | dotnet_naming_symbols.constants.required_modifiers = const 74 | 75 | dotnet_naming_style.constant_style.capitalization = pascal_case 76 | 77 | # Static fields are camelCase and start with s_ 78 | dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion 79 | dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields 80 | dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style 81 | 82 | dotnet_naming_symbols.static_fields.applicable_kinds = field 83 | dotnet_naming_symbols.static_fields.required_modifiers = static 84 | dotnet_naming_symbols.static_fields.applicable_accessibilities = private 85 | 86 | dotnet_naming_style.static_field_style.capitalization = camel_case 87 | dotnet_naming_style.static_field_style.required_prefix = s_ 88 | 89 | # Instance fields are camelCase and start with _ 90 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 91 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 92 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 93 | 94 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 95 | dotnet_naming_symbols.instance_fields.applicable_accessibilities = private 96 | 97 | dotnet_naming_style.instance_field_style.capitalization = camel_case 98 | dotnet_naming_style.instance_field_style.required_prefix = _ 99 | 100 | # Locals and parameters are camelCase 101 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 102 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 103 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 104 | 105 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 106 | 107 | dotnet_naming_style.camel_case_style.capitalization = camel_case 108 | 109 | # Local functions are PascalCase 110 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 111 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 112 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style 113 | 114 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 115 | 116 | dotnet_naming_style.local_function_style.capitalization = pascal_case 117 | 118 | # By default, name items with PascalCase 119 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 120 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 121 | dotnet_naming_rule.members_should_be_pascal_case.style = non_private_static_field_style 122 | 123 | dotnet_naming_symbols.all_members.applicable_kinds = * 124 | 125 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 126 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 127 | dotnet_style_prefer_auto_properties = true:silent 128 | dotnet_style_prefer_collection_expression = true:suggestion 129 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 130 | tab_width = 4 131 | indent_size = 4 132 | end_of_line = crlf 133 | 134 | # CSharp code style settings: 135 | [*.cs] 136 | # Indentation preferences 137 | csharp_indent_block_contents = true 138 | csharp_indent_braces = false 139 | csharp_indent_case_contents = true 140 | csharp_indent_case_contents_when_block = true 141 | csharp_indent_switch_labels = true 142 | csharp_indent_labels = flush_left 143 | 144 | # Prefer "var" everywhere 145 | csharp_style_var_for_built_in_types = true:suggestion 146 | csharp_style_var_when_type_is_apparent = true:suggestion 147 | csharp_style_var_elsewhere = true:suggestion 148 | 149 | # Prefer method-like constructs to have a block body 150 | csharp_style_expression_bodied_methods = false:none 151 | csharp_style_expression_bodied_constructors = false:none 152 | csharp_style_expression_bodied_operators = false:none 153 | 154 | # Prefer property-like constructs to have an expression-body 155 | csharp_style_expression_bodied_properties = true:none 156 | csharp_style_expression_bodied_indexers = true:none 157 | csharp_style_expression_bodied_accessors = true:none 158 | 159 | # Suggest more modern language features when available 160 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 161 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 162 | csharp_style_inlined_variable_declaration = true:suggestion 163 | csharp_style_throw_expression = true:suggestion 164 | csharp_style_conditional_delegate_call = true:suggestion 165 | 166 | # Newline settings 167 | csharp_new_line_before_open_brace = all 168 | csharp_new_line_before_else = true 169 | csharp_new_line_before_catch = true 170 | csharp_new_line_before_finally = true 171 | csharp_new_line_before_members_in_object_initializers = true 172 | csharp_new_line_before_members_in_anonymous_types = true 173 | csharp_new_line_between_query_expression_clauses = true 174 | 175 | # Spacing 176 | csharp_space_after_cast = false 177 | csharp_space_after_colon_in_inheritance_clause = true 178 | csharp_space_after_keywords_in_control_flow_statements = true 179 | csharp_space_around_binary_operators = before_and_after 180 | csharp_space_before_colon_in_inheritance_clause = true 181 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 182 | csharp_space_between_method_call_name_and_opening_parenthesis = false 183 | csharp_space_between_method_call_parameter_list_parentheses = false 184 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 185 | csharp_space_between_method_declaration_parameter_list_parentheses = false 186 | csharp_space_between_parentheses = false 187 | 188 | # Blocks are allowed 189 | csharp_prefer_braces = true:silent 190 | csharp_preserve_single_line_blocks = true 191 | csharp_preserve_single_line_statements = true 192 | # Resharper 193 | csharp_wrap_before_comma = false 194 | csharp_place_attribute_on_same_line = false 195 | csharp_place_constructor_initializer_on_same_line = false 196 | csharp_place_expr_property_on_single_line = true 197 | csharp_place_expr_accessor_on_single_line = true 198 | csharp_wrap_before_first_type_parameter_constraint = true 199 | csharp_wrap_multiple_type_parameter_constraints_style = chop_always 200 | csharp_new_line_before_else = true 201 | csharp_new_line_before_while = true 202 | csharp_place_simple_embedded_block_on_same_line = false 203 | csharp_place_simple_initializer_on_single_line = true 204 | csharp_wrap_object_and_collection_initializer_style = chop_if_long 205 | csharp_wrap_array_initializer_style = chop_if_long 206 | csharp_wrap_after_dot_in_method_calls = false 207 | csharp_wrap_linq_expressions = chop_always 208 | csharp_wrap_before_linq_expression = true 209 | csharp_blank_lines_around_region = 1 210 | csharp_blank_lines_inside_region = 1 211 | csharp_remove_blank_lines_near_braces_in_declarations = true 212 | csharp_keep_blank_lines_in_declarations = 1 213 | csharp_remove_blank_lines_near_braces_in_declarations = true 214 | csharp_blank_lines_after_start_comment = 0 215 | csharp_blank_lines_after_using_list = 1 216 | csharp_blank_lines_inside_namespace = 0 217 | csharp_blank_lines_inside_type = 0 218 | csharp_blank_lines_around_field = 1 219 | csharp_blank_lines_around_single_line_field = 0 220 | csharp_blank_lines_around_property = 1 221 | csharp_blank_lines_around_single_line_property = 1 222 | csharp_blank_lines_around_auto_property = 1 223 | csharp_blank_lines_around_single_line_auto_property = 1 224 | csharp_blank_lines_around_invocable = 1 225 | csharp_blank_lines_around_single_line_invocable = 1 226 | csharp_remove_blank_lines_near_braces_in_code = true 227 | csharp_blank_lines_around_local_method = 1 228 | csharp_blank_lines_around_single_line_local_method = 1 229 | csharp_blank_lines_after_control_transfer_statements = 1 230 | csharp_blank_lines_after_multiline_statements = 1 231 | csharp_default_private_modifier = explicit 232 | csharp_braces_for_ifelse = required_for_multiline_statement 233 | csharp_braces_for_for = required 234 | csharp_braces_for_foreach = required 235 | csharp_braces_for_while = required 236 | csharp_braces_for_dowhile = required 237 | csharp_braces_for_using = required 238 | csharp_braces_for_lock = required 239 | csharp_braces_for_fixed = required 240 | csharp_method_or_operator_body = expression_body 241 | csharp_local_function_body = expression_body 242 | csharp_constructor_or_destructor_body = expression_body 243 | csharp_accessor_owner_body = expression_body 244 | csharp_force_attribute_style = separate 245 | csharp_brace_style = next_line 246 | csharp_anonymous_method_declaration_braces = next_line_shifted_2 247 | csharp_empty_block_style = multiline 248 | csharp_space_within_single_line_array_initializer_braces = true 249 | csharp_style_implicit_object_creation_when_type_is_apparent=false:suggestion 250 | csharp_style_namespace_declarations = file_scoped:warning 251 | csharp_using_directive_placement = outside_namespace:silent 252 | csharp_prefer_simple_using_statement = true:suggestion 253 | csharp_style_prefer_method_group_conversion = true:silent 254 | csharp_style_prefer_top_level_statements = true:silent 255 | csharp_style_prefer_primary_constructors = true:suggestion 256 | csharp_style_expression_bodied_lambdas = true:silent 257 | csharp_style_expression_bodied_local_functions = false:silent -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 8.0.100-rc.1.23463.5 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | - name: Test 28 | run: dotnet test --no-build --verbosity normal 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from `dotnet new gitignore` 5 | 6 | # dotenv files 7 | .env 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # Tye 69 | .tye/ 70 | 71 | # ASP.NET Scaffolding 72 | ScaffoldingReadMe.txt 73 | 74 | # StyleCop 75 | StyleCopReport.xml 76 | 77 | # Files built by Visual Studio 78 | *_i.c 79 | *_p.c 80 | *_h.h 81 | *.ilk 82 | *.meta 83 | *.obj 84 | *.iobj 85 | *.pch 86 | *.pdb 87 | *.ipdb 88 | *.pgc 89 | *.pgd 90 | *.rsp 91 | *.sbr 92 | *.tlb 93 | *.tli 94 | *.tlh 95 | *.tmp 96 | *.tmp_proj 97 | *_wpftmp.csproj 98 | *.log 99 | *.tlog 100 | *.vspscc 101 | *.vssscc 102 | .builds 103 | *.pidb 104 | *.svclog 105 | *.scc 106 | 107 | # Chutzpah Test files 108 | _Chutzpah* 109 | 110 | # Visual C++ cache files 111 | ipch/ 112 | *.aps 113 | *.ncb 114 | *.opendb 115 | *.opensdf 116 | *.sdf 117 | *.cachefile 118 | *.VC.db 119 | *.VC.VC.opendb 120 | 121 | # Visual Studio profiler 122 | *.psess 123 | *.vsp 124 | *.vspx 125 | *.sap 126 | 127 | # Visual Studio Trace Files 128 | *.e2e 129 | 130 | # TFS 2012 Local Workspace 131 | $tf/ 132 | 133 | # Guidance Automation Toolkit 134 | *.gpState 135 | 136 | # ReSharper is a .NET coding add-in 137 | _ReSharper*/ 138 | *.[Rr]e[Ss]harper 139 | *.DotSettings.user 140 | 141 | # TeamCity is a build add-in 142 | _TeamCity* 143 | 144 | # DotCover is a Code Coverage Tool 145 | *.dotCover 146 | 147 | # AxoCover is a Code Coverage Tool 148 | .axoCover/* 149 | !.axoCover/settings.json 150 | 151 | # Coverlet is a free, cross platform Code Coverage Tool 152 | coverage*.json 153 | coverage*.xml 154 | coverage*.info 155 | 156 | # Visual Studio code coverage results 157 | *.coverage 158 | *.coveragexml 159 | 160 | # NCrunch 161 | _NCrunch_* 162 | .*crunch*.local.xml 163 | nCrunchTemp_* 164 | 165 | # MightyMoose 166 | *.mm.* 167 | AutoTest.Net/ 168 | 169 | # Web workbench (sass) 170 | .sass-cache/ 171 | 172 | # Installshield output folder 173 | [Ee]xpress/ 174 | 175 | # DocProject is a documentation generator add-in 176 | DocProject/buildhelp/ 177 | DocProject/Help/*.HxT 178 | DocProject/Help/*.HxC 179 | DocProject/Help/*.hhc 180 | DocProject/Help/*.hhk 181 | DocProject/Help/*.hhp 182 | DocProject/Help/Html2 183 | DocProject/Help/html 184 | 185 | # Click-Once directory 186 | publish/ 187 | 188 | # Publish Web Output 189 | *.[Pp]ublish.xml 190 | *.azurePubxml 191 | # Note: Comment the next line if you want to checkin your web deploy settings, 192 | # but database connection strings (with potential passwords) will be unencrypted 193 | *.pubxml 194 | *.publishproj 195 | 196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 197 | # checkin your Azure Web App publish settings, but sensitive information contained 198 | # in these scripts will be unencrypted 199 | PublishScripts/ 200 | 201 | # NuGet Packages 202 | *.nupkg 203 | # NuGet Symbol Packages 204 | *.snupkg 205 | # The packages folder can be ignored because of Package Restore 206 | **/[Pp]ackages/* 207 | # except build/, which is used as an MSBuild target. 208 | !**/[Pp]ackages/build/ 209 | # Uncomment if necessary however generally it will be regenerated when needed 210 | #!**/[Pp]ackages/repositories.config 211 | # NuGet v3's project.json files produces more ignorable files 212 | *.nuget.props 213 | *.nuget.targets 214 | 215 | # Microsoft Azure Build Output 216 | csx/ 217 | *.build.csdef 218 | 219 | # Microsoft Azure Emulator 220 | ecf/ 221 | rcf/ 222 | 223 | # Windows Store app package directories and files 224 | AppPackages/ 225 | BundleArtifacts/ 226 | Package.StoreAssociation.xml 227 | _pkginfo.txt 228 | *.appx 229 | *.appxbundle 230 | *.appxupload 231 | 232 | # Visual Studio cache files 233 | # files ending in .cache can be ignored 234 | *.[Cc]ache 235 | # but keep track of directories ending in .cache 236 | !?*.[Cc]ache/ 237 | 238 | # Others 239 | ClientBin/ 240 | ~$* 241 | *~ 242 | *.dbmdl 243 | *.dbproj.schemaview 244 | *.jfm 245 | *.pfx 246 | *.publishsettings 247 | orleans.codegen.cs 248 | 249 | # Including strong name files can present a security risk 250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 251 | #*.snk 252 | 253 | # Since there are multiple workflows, uncomment next line to ignore bower_components 254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 255 | #bower_components/ 256 | 257 | # RIA/Silverlight projects 258 | Generated_Code/ 259 | 260 | # Backup & report files from converting an old project file 261 | # to a newer Visual Studio version. Backup files are not needed, 262 | # because we have git ;-) 263 | _UpgradeReport_Files/ 264 | Backup*/ 265 | UpgradeLog*.XML 266 | UpgradeLog*.htm 267 | ServiceFabricBackup/ 268 | *.rptproj.bak 269 | 270 | # SQL Server files 271 | *.mdf 272 | *.ldf 273 | *.ndf 274 | 275 | # Business Intelligence projects 276 | *.rdl.data 277 | *.bim.layout 278 | *.bim_*.settings 279 | *.rptproj.rsuser 280 | *- [Bb]ackup.rdl 281 | *- [Bb]ackup ([0-9]).rdl 282 | *- [Bb]ackup ([0-9][0-9]).rdl 283 | 284 | # Microsoft Fakes 285 | FakesAssemblies/ 286 | 287 | # GhostDoc plugin setting file 288 | *.GhostDoc.xml 289 | 290 | # Node.js Tools for Visual Studio 291 | .ntvs_analysis.dat 292 | node_modules/ 293 | 294 | # Visual Studio 6 build log 295 | *.plg 296 | 297 | # Visual Studio 6 workspace options file 298 | *.opt 299 | 300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 301 | *.vbw 302 | 303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 304 | *.vbp 305 | 306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 307 | *.dsw 308 | *.dsp 309 | 310 | # Visual Studio 6 technical files 311 | *.ncb 312 | *.aps 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # CodeRush personal settings 330 | .cr/personal 331 | 332 | # Python Tools for Visual Studio (PTVS) 333 | __pycache__/ 334 | *.pyc 335 | 336 | # Cake - Uncomment if you are using it 337 | # tools/** 338 | # !tools/packages.config 339 | 340 | # Tabs Studio 341 | *.tss 342 | 343 | # Telerik's JustMock configuration file 344 | *.jmconfig 345 | 346 | # BizTalk build output 347 | *.btp.cs 348 | *.btm.cs 349 | *.odx.cs 350 | *.xsd.cs 351 | 352 | # OpenCover UI analysis results 353 | OpenCover/ 354 | 355 | # Azure Stream Analytics local run output 356 | ASALocalRun/ 357 | 358 | # MSBuild Binary and Structured Log 359 | *.binlog 360 | 361 | # NVidia Nsight GPU debugger configuration file 362 | *.nvuser 363 | 364 | # MFractors (Xamarin productivity tool) working folder 365 | .mfractor/ 366 | 367 | # Local History for Visual Studio 368 | .localhistory/ 369 | 370 | # Visual Studio History (VSHistory) files 371 | .vshistory/ 372 | 373 | # BeatPulse healthcheck temp database 374 | healthchecksdb 375 | 376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 377 | MigrationBackup/ 378 | 379 | # Ionide (cross platform F# VS Code tools) working folder 380 | .ionide/ 381 | 382 | # Fody - auto-generated XML schema 383 | FodyWeavers.xsd 384 | 385 | # VS Code files for those working on multiple tools 386 | .vscode/* 387 | !.vscode/settings.json 388 | !.vscode/tasks.json 389 | !.vscode/launch.json 390 | !.vscode/extensions.json 391 | *.code-workspace 392 | 393 | # Local History for Visual Studio Code 394 | .history/ 395 | 396 | # Windows Installer files from build outputs 397 | *.cab 398 | *.msi 399 | *.msix 400 | *.msm 401 | *.msp 402 | 403 | # JetBrains Rider 404 | *.sln.iml 405 | .idea 406 | 407 | ## 408 | ## Visual studio for Mac 409 | ## 410 | 411 | 412 | # globs 413 | Makefile.in 414 | *.userprefs 415 | *.usertasks 416 | config.make 417 | config.status 418 | aclocal.m4 419 | install-sh 420 | autom4te.cache/ 421 | *.tar.gz 422 | tarballs/ 423 | test-results/ 424 | 425 | # Mac bundle stuff 426 | *.dmg 427 | *.app 428 | 429 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 430 | # General 431 | .DS_Store 432 | .AppleDouble 433 | .LSOverride 434 | 435 | # Icon must end with two \r 436 | Icon 437 | 438 | 439 | # Thumbnails 440 | ._* 441 | 442 | # Files that might appear in the root of a volume 443 | .DocumentRevisions-V100 444 | .fseventsd 445 | .Spotlight-V100 446 | .TemporaryItems 447 | .Trashes 448 | .VolumeIcon.icns 449 | .com.apple.timemachine.donotpresent 450 | 451 | # Directories potentially created on remote AFP share 452 | .AppleDB 453 | .AppleDesktop 454 | Network Trash Folder 455 | Temporary Items 456 | .apdisk 457 | 458 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 459 | # Windows thumbnail cache files 460 | Thumbs.db 461 | ehthumbs.db 462 | ehthumbs_vista.db 463 | 464 | # Dump file 465 | *.stackdump 466 | 467 | # Folder config file 468 | [Dd]esktop.ini 469 | 470 | # Recycle Bin used on file shares 471 | $RECYCLE.BIN/ 472 | 473 | # Windows Installer files 474 | *.cab 475 | *.msi 476 | *.msix 477 | *.msm 478 | *.msp 479 | 480 | # Windows shortcuts 481 | *.lnk 482 | 483 | # Vim temporary swap files 484 | *.swp 485 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | false 5 | Nullable 6 | enable 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ilchert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NuGet](https://buildstats.info/nuget/SystemTextJson.FluentApi)](https://www.nuget.org/packages/SystemTextJson.FluentApi/ "Download SystemTextJson.FluentApi from NuGet.org") 2 | 3 | Normally you do not need to use this package and just copy paste required functionality like NRT or polymorphism support. 4 | # SystemTextJson.FluentApi 5 | SystemTextJson.FluentApi is a fluent configuration library for System.Text.Json that allows developers to configure serialization uses strongly typed fluent interface and lambda expression. 6 | 7 | # Documentation 8 | All api usually repeats attributes from [System.Text.Json.Serialization](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization) and set corresponding property in [JsonPropertyInfo](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.jsonpropertyinfo?view=net-7.0) or [JsonTypeInfo](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.jsontypeinfo?view=net-7.0). Configuration based on [IJsonTypeInfoResolver](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.ijsontypeinforesolver) so developers can configure reflection based [TypeInfoResolver](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.defaultjsontypeinforesolver) and source generator [JsonSerializerContext](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonserializercontext). 9 | 10 | # Quick start 11 | To use FluentApi need to configure [JsonSerializerOptions](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions) instance via `JsonModelBuilder` and pass it to serializer. 12 | 13 | ```C# 14 | var options = new JsonSerializerOptions() { WriteIndented = true }; 15 | options.ConfigureDefaultTypeResolver(p => 16 | p.Entity() 17 | .Property(p => p.LastName).HasName("surname") 18 | .Property(p => p.FirstName).IsIgnored() 19 | .VirtualProperty("FullName", p => $"{p.FirstName} {p.LastName}") 20 | .Property(p => p.Age).HasHumberHandling(JsonNumberHandling.WriteAsString)); 21 | 22 | var person = new Person() { FirstName = "First name", LastName = "Last name", Age = 12 }; 23 | var json = JsonSerializer.Serialize(person, options); 24 | 25 | Console.WriteLine(json); 26 | ``` 27 | 28 | This example produce this JSON 29 | ```Json 30 | { 31 | "surname": "Last name", 32 | "Age": "12", 33 | "FullName": "First name Last name" 34 | } 35 | ``` 36 | 37 | # Polymorphism serialization 38 | STJ has build in support polymorphic serialization, but user have to annotate base class with [JsonDerivedTypeAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonderivedtypeattribute) with all derived types. In fluent API you can configure each derived type manually or find all derived types in runtime. 39 | 40 | ```C# 41 | builder.Entity() 42 | .HasDerivedType(nameof(Derived1)) 43 | .HasDerivedType(nameof(Derived2)) 44 | .HasDerivedType(nameof(Root)) 45 | // or 46 | builder.Entity().HasDerivedTypesFromAssembly(Assembly.GetExecutingAssembly(), t => t.Name) 47 | 48 | var testObject = new Root[] 49 | { 50 | new Derived1() { Derived1Property = "derived" }, 51 | new Derived2() { Derived2Property = "derived2" }, 52 | new Root(){ RootProperty = "root"} 53 | }; 54 | 55 | 56 | public class Root 57 | { 58 | public string? RootProperty { get; set; } 59 | } 60 | 61 | public class Derived1 : Root 62 | { 63 | public string? Derived1Property { get; set; } 64 | } 65 | 66 | public class Derived2 : Root 67 | { 68 | public string? Derived2Property { get; set; } 69 | } 70 | ``` 71 | 72 | Serialization of `testObject` collection will produce: 73 | 74 | ```JSON 75 | [ 76 | { 77 | "$type":"Derived1", 78 | "Derived1Property":"derived", 79 | "RootProperty":null 80 | }, 81 | { 82 | "$type":"Derived2", 83 | "Derived2Property":"derived2", 84 | "RootProperty":null 85 | }, 86 | { 87 | "$type":"Root", 88 | "RootProperty":"root" 89 | } 90 | ] 91 | ``` 92 | 93 | With `$type` discriminator serializer are able to deserialize this collection. Another approach to serialization is use actual type from object instance, instead of property type. To achieve this behavior serializer can threat specific property as `object` using `PropertyBuilder.SerializeAsObject`. 94 | 95 | ```C# 96 | builder.Entity().Property(p => p.Data).SerializeAsObject(); 97 | 98 | var testObject = new AsObjectTestClass { Data = new Derived() { Property = "Prop" } }; 99 | 100 | public class AsObjectTestClass 101 | { 102 | public Root? Data { get; set; } 103 | } 104 | 105 | public class Root { } 106 | 107 | public class Derived : Root 108 | { 109 | public string? Property { get; set; } 110 | } 111 | 112 | ``` 113 | 114 | Serialization of `testObject` will produce: 115 | ```JSON 116 | { 117 | "Data":{ 118 | "Property":"Prop" 119 | } 120 | } 121 | ``` 122 | 123 | But in this case only serialization is available because JSON does not contain type discriminator and `JsonException` will be thrown on deserialization. 124 | 125 | # Nullable reference type support 126 | STJ has build in support of [`required`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/required) properties, but it just check, that value exists in JSON on deserialization and does not prevent setting `null` to none nullable properties. Fluent Api can configure `JsonSerializerOptions` to respect NRT annotations on fields and properties. Internally it uses [JsonPropertyInfo.Set](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.jsonpropertyinfo.set) property and reduces deserialization performance. 127 | STJ will implement full NRT support in [.net9](https://github.com/dotnet/runtime/issues/100144) 128 | 129 | ```C# 130 | builder.RespectNullableReferenceType(); 131 | 132 | JsonSerializer.Deserialize("""{"Property": null}""", _options); // this throws JsonException 133 | JsonAsserts.AssertObject(new TestClass(), "{}", _options); // BUT this is not because Property is not requred. 134 | 135 | public class TestClass 136 | { 137 | public string Property { get; set; } 138 | } 139 | ``` 140 | 141 | # Virtual properties 142 | 143 | Fluent Api can define virtual properties, that does not match to any real property in object. 144 | 145 | ```C# 146 | builder.Entity() 147 | .Property(p => p.LastName).IsIgnored() 148 | .Property(p => p.FirstName).IsIgnored() 149 | .VirtualProperty("FullName", p => $"{p.FirstName} {p.LastName}") 150 | 151 | var testObject = new Person() { FirstName = "First name", LastName = "Last name" }; 152 | 153 | class Person 154 | { 155 | public string? FirstName { get; set; } 156 | 157 | public string? LastName { get; set; } 158 | } 159 | 160 | ``` 161 | 162 | Serialization of `testObject` will produce: 163 | 164 | ```JSON 165 | { 166 | "FullName": "First name Last name" 167 | } 168 | ``` 169 | 170 | # Change tracking 171 | 172 | Fluent Api can track changes during serialization and deserialization. If some entity implement `IHaveChnagedProperties` interface with not null `ChangedProperties` property, it will be used to track changes. To populate property/field names that set deserialization use `TrackChangedProperties()` method. To serialize properties only from `ChangedProperties` use `SerializeOnlyChangedProperties()`. This method will override `JsonIgnoreCondition`. 173 | 174 | ```C# 175 | builder.TrackChangedProperties().SerializeOnlyChangedProperties(); 176 | 177 | var testObject = new TrackTestClass() 178 | { 179 | StringProperty = "str", 180 | IntProperty = 1, 181 | ChangedProperties = { nameof(TrackTestClass.IntProperty) } 182 | }; 183 | 184 | public class TrackTestClass : IHaveChangedProperties 185 | { 186 | public string? StringProperty { get; set; } 187 | public int IntProperty { get; set; } 188 | public ISet ChangedProperties { get; } = new HashSet(); 189 | } 190 | 191 | ``` 192 | 193 | Serialization of `testObject` will produce: 194 | 195 | ```JSON 196 | { 197 | "IntProperty": 1 198 | } 199 | ``` 200 | And deserialization of this JSON will populate `"IntProperty"` value to `ChangedProperties`. 201 | 202 | # ValueTuple serialization 203 | 204 | Fluent Api has `ValueTupleJsonConverter` to serialize and deserialize `ValueTuple` as array. 205 | 206 | ```C# 207 | var options = new JsonSerializerOptions() { Converters = { new ValueTupleJsonConverter() } }; 208 | JsonSerializer.Serialize((1,"str"),options); 209 | ``` 210 | 211 | This code output: 212 | 213 | ```JSON 214 | [1,"str"] 215 | ``` 216 | 217 | # Inline arrays support 218 | 219 | Fluent Api has `InlineArrayJsonConverter` for .NET 8 and above to serialize and deserialize [`InlineArray`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/inline-arrays) structs as arrays. 220 | 221 | ```C# 222 | var array = new InlineArray(); 223 | array[0] = null; 224 | array[1] = 1; 225 | array[2] = -1; 226 | var options = new JsonSerializerOptions() { Converters = { new InlineArrayJsonConverter() } }; 227 | JsonSerializer.Serialize(array,options); 228 | 229 | [InlineArray(3)] 230 | private struct InlineArray 231 | { 232 | public int? Value; 233 | } 234 | ``` 235 | 236 | Output: `"[null,1,-1]"` 237 | 238 | # Enum with JsonPropertyName attributes 239 | 240 | Default [JsonStringEnumConverter](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonstringenumconverter) does not respect `JsonPropertyNameAttribute`, to fix it use CustomizableJsonStringEnumConverter instead. 241 | 242 | ```C# 243 | // set globally 244 | var options = new JsonSerializerOptions() { Converters = { new CustomizableJsonStringEnumConverter(JsonNamingPolicy.CamelCase) } }; 245 | // or for scpecific enum 246 | var options = new JsonSerializerOptions().ConfigureEnumValues( 247 | new Dictionary { { A.First, "f" } }, 248 | namingPolicy: JsonNamingPolicy.CamelCase); 249 | JsonSerializer.Serialize([A.First, null, A.Third, (A)8], _options); 250 | 251 | enum A 252 | { 253 | [JsonPropertyName("f")] 254 | First, 255 | Second, 256 | Third 257 | } 258 | ``` 259 | 260 | Output: 261 | ```JS 262 | ["f",null,"third",8] 263 | ``` 264 | -------------------------------------------------------------------------------- /SystemTextJson.FluentApi.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34112.27 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemTextJson.FluentApi", "src\SystemTextJson.FluentApi\SystemTextJson.FluentApi.csproj", "{BE2BD318-C02F-4ED0-B9E5-87C7C93C0D33}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemTextJson.FluentApi.Tests", "tests\SystemTextJson.FluentApi.Tests\SystemTextJson.FluentApi.Tests.csproj", "{1AD3BC43-4E56-418E-BA9F-11DDD0028C74}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemTextJson.FluentApi.QuickStart", "samples\SystemTextJson.FluentApi.QuickStart\SystemTextJson.FluentApi.QuickStart.csproj", "{220BD50B-2A8F-4000-B984-3AB75FB24D93}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{D66C1040-8D07-436A-AD30-E1888B556441}" 13 | ProjectSection(SolutionItems) = preProject 14 | .editorconfig = .editorconfig 15 | Directory.Build.props = Directory.Build.props 16 | LICENSE = LICENSE 17 | README.md = README.md 18 | EndProjectSection 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {BE2BD318-C02F-4ED0-B9E5-87C7C93C0D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {BE2BD318-C02F-4ED0-B9E5-87C7C93C0D33}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {BE2BD318-C02F-4ED0-B9E5-87C7C93C0D33}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {BE2BD318-C02F-4ED0-B9E5-87C7C93C0D33}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {1AD3BC43-4E56-418E-BA9F-11DDD0028C74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {1AD3BC43-4E56-418E-BA9F-11DDD0028C74}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {1AD3BC43-4E56-418E-BA9F-11DDD0028C74}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {1AD3BC43-4E56-418E-BA9F-11DDD0028C74}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {220BD50B-2A8F-4000-B984-3AB75FB24D93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {220BD50B-2A8F-4000-B984-3AB75FB24D93}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {220BD50B-2A8F-4000-B984-3AB75FB24D93}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {220BD50B-2A8F-4000-B984-3AB75FB24D93}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(ExtensibilityGlobals) = postSolution 43 | SolutionGuid = {03C213A2-7B38-4322-AA09-5C73DE365869} 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /samples/SystemTextJson.FluentApi.QuickStart/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using SystemTextJson.FluentApi; 5 | 6 | var options = new JsonSerializerOptions() { WriteIndented = true, Converters = { new ValueTupleJsonConverter() } }; 7 | options.ConfigureDefaultTypeResolver(p => 8 | p.Entity() 9 | .Property(p => p.LastName).HasName("surname") 10 | .Property(p => p.FirstName).IsIgnored() 11 | .VirtualProperty("FullName", p => $"{p?.FirstName} {p?.LastName}") 12 | .Property(p => p.Age).HasHumberHandling(JsonNumberHandling.WriteAsString)); 13 | 14 | var person = new Person() { FirstName = "First name", LastName = "Last name", Age = 12 }; 15 | var json = JsonSerializer.Serialize(person, options); 16 | 17 | Console.WriteLine(json); 18 | 19 | var tupleJson = JsonSerializer.Serialize((1,"str"),options); 20 | Console.WriteLine(tupleJson); 21 | 22 | class Person 23 | { 24 | public string? FirstName { get; set; } 25 | 26 | public string? LastName { get; set; } 27 | 28 | public int Age { get; set; } 29 | } 30 | -------------------------------------------------------------------------------- /samples/SystemTextJson.FluentApi.QuickStart/SystemTextJson.FluentApi.QuickStart.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/CustomEnumConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace SystemTextJson.FluentApi; 6 | 7 | internal class CustomEnumConverter : JsonConverter 8 | where T : struct, Enum 9 | { 10 | private readonly JsonConverter _inner; 11 | private readonly Dictionary _valuesCache; 12 | private readonly Dictionary _namesCache; 13 | 14 | public CustomEnumConverter(JsonConverter inner, JsonSerializerOptions options) 15 | { 16 | _inner = inner; 17 | _valuesCache = []; 18 | _namesCache = []; 19 | 20 | foreach (var field in typeof(T).GetFields()) 21 | { 22 | if (!field.IsLiteral) 23 | continue; 24 | 25 | var attribute = field.GetCustomAttribute(); 26 | if (attribute is null) 27 | continue; 28 | 29 | _valuesCache[attribute.Name] = (T)field.GetRawConstantValue()!; 30 | _namesCache[(T)field.GetRawConstantValue()!] = JsonEncodedText.Encode(attribute.Name, options.Encoder); 31 | } 32 | 33 | _inner = inner; 34 | } 35 | 36 | public CustomEnumConverter(JsonConverter inner, JsonSerializerOptions options, IReadOnlyDictionary mapping) 37 | { 38 | _inner = inner; 39 | _valuesCache = []; 40 | _namesCache = []; 41 | 42 | foreach (var kv in mapping) 43 | { 44 | _valuesCache[kv.Value] = kv.Key; 45 | _namesCache[kv.Key] = JsonEncodedText.Encode(kv.Value, options.Encoder); 46 | } 47 | 48 | _inner = inner; 49 | } 50 | 51 | public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 52 | { 53 | if (reader.TokenType == JsonTokenType.String) 54 | { 55 | var valueStr = reader.GetString(); 56 | if (valueStr is null) 57 | return default; 58 | 59 | if (_valuesCache.TryGetValue(valueStr, out var value)) 60 | return value; 61 | } 62 | 63 | return _inner.Read(ref reader, typeToConvert, options); 64 | } 65 | 66 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) 67 | { 68 | if (_namesCache.TryGetValue(value, out var valueStr)) 69 | writer.WriteStringValue(valueStr); 70 | else 71 | _inner.Write(writer, value, options); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/CustomizableJsonStringEnumConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace SystemTextJson.FluentApi; 6 | public class CustomizableJsonStringEnumConverter : JsonConverterFactory 7 | { 8 | private readonly JsonStringEnumConverter _defaultConverter; 9 | public CustomizableJsonStringEnumConverter() => 10 | _defaultConverter = new JsonStringEnumConverter(); 11 | 12 | public CustomizableJsonStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true) => 13 | _defaultConverter = new JsonStringEnumConverter(namingPolicy, allowIntegerValues); 14 | 15 | public override bool CanConvert(Type typeToConvert) => 16 | _defaultConverter.CanConvert(typeToConvert); 17 | 18 | public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) 19 | { 20 | var defaultConverter = _defaultConverter.CreateConverter(typeToConvert, options); 21 | 22 | foreach (var field in typeToConvert.GetFields()) 23 | { 24 | if (!field.IsLiteral) 25 | continue; 26 | 27 | var attribute = field.GetCustomAttribute(); 28 | if (attribute is null) 29 | continue; 30 | 31 | var type = typeof(CustomEnumConverter<>).MakeGenericType(typeToConvert); 32 | return (JsonConverter)Activator.CreateInstance(type, defaultConverter, options)!; 33 | } 34 | 35 | return defaultConverter; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/EntityTypeBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization.Metadata; 2 | 3 | namespace SystemTextJson.FluentApi; 4 | 5 | public class EntityTypeBuilder(JsonModelBuilder modelBuilder) : IEntityTypeBuilder 6 | { 7 | public Type EntityType => typeof(TEntity); 8 | public IList> JsonTypeInfoActions { get; } = []; 9 | public IList PropertyBuilders { get; } = []; 10 | 11 | public JsonModelBuilder ModelBuilder { get; } = modelBuilder; 12 | 13 | public EntityTypeBuilder ConfigureTyped(Action> configureAction) 14 | { 15 | JsonTypeInfoActions.Add(p => configureAction((JsonTypeInfo)p)); 16 | return this; 17 | } 18 | 19 | public EntityTypeBuilder HasDerivedType() where T : TEntity => 20 | this.HasDerivedType(typeof(T)); 21 | 22 | public EntityTypeBuilder HasDerivedType(string typeDiscriminator) where T : TEntity => 23 | this.HasDerivedType(typeof(T), typeDiscriminator); 24 | 25 | public EntityTypeBuilder HasDerivedType(int typeDiscriminator) where T : TEntity => 26 | this.HasDerivedType(typeof(T), typeDiscriminator); 27 | 28 | Action IEntityTypeBuilder.Build() 29 | { 30 | var memberProperties = PropertyBuilders.OfType() 31 | .GroupBy(p => p.MemberInfo, p => p.Build()) 32 | .Select(p => (p.Key, Value: (Action)Delegate.Combine(p.ToArray())!)) 33 | .ToDictionary(p => p.Key, p => p.Value); 34 | 35 | var namedProperties = PropertyBuilders.Where(p => p is not IMemberPropertyBuilder). 36 | GroupBy(p => p.Name, p => p.Build()) 37 | .Select(p => (p.Key, Value: (Action)Delegate.Combine(p.ToArray())!)) 38 | .ToDictionary(p => p.Key, p => p.Value); 39 | 40 | var typeConfigurations = JsonTypeInfoActions.ToArray(); 41 | return p => 42 | { 43 | foreach (var tc in typeConfigurations) 44 | tc(p); 45 | 46 | foreach (var prop in p.Properties) 47 | { 48 | var mi = prop.GetMemberInfo(); 49 | Action? propertyConfig = null; 50 | 51 | if (mi is null) 52 | namedProperties.TryGetValue(prop.Name, out propertyConfig); 53 | else 54 | memberProperties.TryGetValue(mi, out propertyConfig); 55 | 56 | propertyConfig?.Invoke(prop); 57 | } 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/EntityTypeBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | using System.Text.Json.Serialization; 4 | using System.Text.Json.Serialization.Metadata; 5 | 6 | namespace SystemTextJson.FluentApi; 7 | 8 | public static class EntityTypeBuilderExtensions 9 | { 10 | public static T Configure(this T builder, Action configureAction) where T : IEntityTypeBuilder 11 | { 12 | builder.JsonTypeInfoActions.Add(configureAction); 13 | return builder; 14 | } 15 | 16 | public static T IsUnmappedMemberDisallowed(this T builder) where T : IEntityTypeBuilder => 17 | builder.Configure(p => p.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow); 18 | 19 | public static T HasTypeDiscriminator(this T builder, string typeDiscriminator) where T : IEntityTypeBuilder => 20 | builder.Configure(p => 21 | { 22 | p.PolymorphismOptions ??= new JsonPolymorphismOptions(); 23 | p.PolymorphismOptions.TypeDiscriminatorPropertyName = typeDiscriminator; 24 | }); 25 | 26 | public static T HasDerivedType(this T builder, Type derivedType) where T : IEntityTypeBuilder => 27 | builder.HasDerivedType(new JsonDerivedType(derivedType)); 28 | 29 | public static T HasDerivedType(this T builder, Type derivedType, string typeDiscriminator) where T : IEntityTypeBuilder => 30 | builder.HasDerivedType(new JsonDerivedType(derivedType, typeDiscriminator)); 31 | 32 | public static T HasDerivedType(this T builder, Type derivedType, int typeDiscriminator) where T : IEntityTypeBuilder => 33 | builder.HasDerivedType(new JsonDerivedType(derivedType, typeDiscriminator)); 34 | 35 | public static T HasDerivedType(this T builder, JsonDerivedType derivedType) where T : IEntityTypeBuilder 36 | { 37 | return builder.Configure(p => 38 | { 39 | p.PolymorphismOptions ??= new JsonPolymorphismOptions(); 40 | p.PolymorphismOptions.DerivedTypes.Add(derivedType); 41 | }); 42 | } 43 | 44 | public static T HasDerivedType(this T builder, params JsonDerivedType[] derivedTypes) where T : IEntityTypeBuilder 45 | { 46 | return builder.Configure(p => 47 | { 48 | p.PolymorphismOptions ??= new JsonPolymorphismOptions(); 49 | foreach (var derivedType in derivedTypes) 50 | p.PolymorphismOptions.DerivedTypes.Add(derivedType); 51 | }); 52 | } 53 | 54 | public static T HasDerivedTypesFromAssembly(this T builder, Assembly assembly, Func? discriminatorFormatter = null) where T : IEntityTypeBuilder 55 | { 56 | Type[] types; 57 | try 58 | { 59 | types = assembly.GetTypes(); 60 | } 61 | catch (ReflectionTypeLoadException ex) 62 | { 63 | types = ex.Types.Where(p => p != null).ToArray()!; 64 | } 65 | 66 | types = types.Where(p => p != null && builder.EntityType.IsAssignableFrom(p)).ToArray(); 67 | if (types.Length == 0) 68 | return builder; 69 | 70 | var jsonTypes = discriminatorFormatter is null ? 71 | types.Select(p => new JsonDerivedType(p)) : 72 | types.Select(p => new JsonDerivedType(p, discriminatorFormatter(p))); 73 | 74 | builder.HasDerivedType(jsonTypes.ToArray()); 75 | 76 | return builder; 77 | } 78 | 79 | public static VirtualPropertyBuilder VirtualProperty(this IEntityTypeBuilder builder, string name, Func compute) 80 | { 81 | builder.Configure(p => 82 | { 83 | if (p.Properties.Any(p => p.Name == name)) 84 | return; 85 | var propInfo = p.CreateJsonPropertyInfo(typeof(TProperty), name); 86 | propInfo.Get = (o) => compute((TEntity)o); 87 | p.Properties.Add(propInfo); 88 | }); 89 | var newBuilder = new VirtualPropertyBuilder(name, builder); 90 | builder.PropertyBuilders.Add(newBuilder); 91 | return newBuilder; 92 | } 93 | 94 | 95 | public static MemberPropertyBuilder Property(this IEntityTypeBuilder builder, Expression> propertyExpression) 96 | { 97 | var mi = GetMemberInfo(propertyExpression); 98 | var newBuilder = new MemberPropertyBuilder(mi, builder); 99 | builder.PropertyBuilders.Add(newBuilder); 100 | return newBuilder; 101 | } 102 | 103 | private static MemberInfo GetMemberInfo(Expression> propertyExpression) 104 | { 105 | if (propertyExpression.Body is not MemberExpression member) 106 | throw new ArgumentException($"Expression '{propertyExpression}' refers to a method, not a property."); 107 | 108 | if (member.Member is not (PropertyInfo or FieldInfo)) 109 | throw new ArgumentException($"Expression '{propertyExpression}' refers to a field, not a property."); 110 | 111 | return member.Member; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/IEntityTypeBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json.Serialization.Metadata; 3 | 4 | namespace SystemTextJson.FluentApi; 5 | 6 | public interface IEntityTypeBuilder 7 | { 8 | Type EntityType { get; } 9 | internal IList> JsonTypeInfoActions { get; } 10 | 11 | internal IList PropertyBuilders { get; } 12 | 13 | JsonModelBuilder ModelBuilder { get; } 14 | 15 | internal Action Build(); 16 | } 17 | 18 | public interface IEntityTypeBuilder : IEntityTypeBuilder 19 | { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/IHaveChangedProperties.cs: -------------------------------------------------------------------------------- 1 | namespace SystemTextJson.FluentApi; 2 | public interface IHaveChangedProperties 3 | { 4 | ISet? ChangedProperties { get; } 5 | } 6 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/IMemberPropertyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace SystemTextJson.FluentApi; 4 | 5 | public interface IMemberPropertyBuilder : IPropertyBuilder 6 | { 7 | public MemberInfo MemberInfo { get; } 8 | } 9 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/IPropertyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization.Metadata; 2 | 3 | namespace SystemTextJson.FluentApi; 4 | 5 | public interface IPropertyBuilder 6 | { 7 | string Name { get; } 8 | 9 | internal IList> JsonPropertyInfoActions { get; } 10 | 11 | internal IEntityTypeBuilder EntityTypeBuilder { get; } 12 | 13 | internal Action Build(); 14 | } 15 | public interface IPropertyBuilder : IPropertyBuilder 16 | { 17 | new IEntityTypeBuilder EntityTypeBuilder { get; } 18 | } 19 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/InlineArrayJsonConverter.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Reflection; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using System.Runtime.CompilerServices; 6 | using System.Runtime.InteropServices; 7 | using static SystemTextJson.FluentApi.SerializationHelpers; 8 | namespace SystemTextJson.FluentApi; 9 | #if NET8_0_OR_GREATER 10 | 11 | public class InlineArrayJsonConverter : JsonConverterFactory 12 | { 13 | public override bool CanConvert(Type typeToConvert) => 14 | typeToConvert.GetCustomAttribute() != null; 15 | 16 | public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) 17 | { 18 | var attribute = typeToConvert.GetCustomAttribute(); 19 | if (attribute is null) 20 | return null; 21 | 22 | var length = attribute.Length; 23 | var itemType = typeToConvert.GetFields()[0].FieldType; // inline array can have only one field 24 | 25 | var converterType = typeof(ConcreteInlineArrayJsonConverter<,>).MakeGenericType(typeToConvert, itemType); 26 | return (JsonConverter)Activator.CreateInstance(converterType, length, options)!; 27 | } 28 | 29 | private class ConcreteInlineArrayJsonConverter(int length, JsonSerializerOptions options) : JsonConverter 30 | where TStruct : struct 31 | { 32 | private readonly JsonConverter _itemConverter = (JsonConverter)options.GetConverter(typeof(TItem)); 33 | public override TStruct Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 34 | { 35 | if (reader.TokenType != JsonTokenType.StartArray) 36 | throw new JsonException("Start token must be '['."); 37 | 38 | reader.Read(); 39 | 40 | var result = default(TStruct); 41 | var span = MemoryMarshal.CreateSpan(ref Unsafe.As(ref result), length); 42 | for (var i = 0; i < span.Length; i++) 43 | span[i] = ReadValue(_itemConverter, ref reader, options); 44 | 45 | if (reader.TokenType != JsonTokenType.EndArray) 46 | throw new JsonException("Expected end token ']'."); 47 | 48 | return result; 49 | } 50 | 51 | public override void Write(Utf8JsonWriter writer, TStruct value, JsonSerializerOptions options) 52 | { 53 | writer.WriteStartArray(); 54 | 55 | var span = MemoryMarshal.CreateSpan(ref Unsafe.As(ref value), length); 56 | for (var i = 0; i < span.Length; i++) 57 | WriteValue(_itemConverter, writer, span[i], options); 58 | 59 | writer.WriteEndArray(); 60 | } 61 | } 62 | 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/JsonModelBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization.Metadata; 4 | 5 | namespace SystemTextJson.FluentApi; 6 | 7 | public sealed class JsonModelBuilder 8 | { 9 | private readonly Dictionary _configurations = []; 10 | private readonly List> _typeConfigurations = []; 11 | 12 | public EntityTypeBuilder Entity() 13 | { 14 | if (_configurations.TryGetValue(typeof(TEntity), out var entityTypeBuilder)) 15 | return (EntityTypeBuilder)entityTypeBuilder; 16 | 17 | var newBuilder = new EntityTypeBuilder(this); 18 | _configurations[typeof(TEntity)] = newBuilder; 19 | return newBuilder; 20 | } 21 | 22 | public JsonModelBuilder Configure(Action configureAction) 23 | { 24 | _typeConfigurations.Add(configureAction); 25 | return this; 26 | } 27 | 28 | public JsonModelBuilder TrackChangedProperties() 29 | { 30 | Configure(p => 31 | { 32 | if (!typeof(IHaveChangedProperties).IsAssignableFrom(p.Type)) 33 | return; 34 | 35 | for (var i = 0; i < p.Properties.Count; i++) 36 | { 37 | var property = p.Properties[i]; 38 | if (property.GetMemberInfo() is not { } mi) 39 | continue; 40 | 41 | if (mi.Name == nameof(IHaveChangedProperties.ChangedProperties)) 42 | { 43 | p.Properties.RemoveAt(i); 44 | i--; 45 | continue; 46 | } 47 | 48 | if (property.Set is not { } set) 49 | continue; 50 | 51 | var realPropertyName = mi.Name; 52 | 53 | property.Set = (o, value) => 54 | { 55 | var cp = o as IHaveChangedProperties; 56 | cp?.ChangedProperties?.Add(realPropertyName); 57 | set(o, value); 58 | }; 59 | } 60 | }); 61 | 62 | 63 | return this; 64 | } 65 | 66 | public JsonModelBuilder SerializeOnlyChangedProperties() 67 | { 68 | Configure(p => 69 | { 70 | if (!typeof(IHaveChangedProperties).IsAssignableFrom(p.Type)) 71 | return; 72 | 73 | for (var i = 0; i < p.Properties.Count; i++) 74 | { 75 | var property = p.Properties[i]; 76 | if (property.GetMemberInfo() is not { } mi) 77 | continue; 78 | 79 | if (mi.Name == nameof(IHaveChangedProperties.ChangedProperties)) 80 | { 81 | p.Properties.RemoveAt(i); 82 | i--; 83 | continue; 84 | } 85 | 86 | if (property.Get is not { }) 87 | continue; 88 | 89 | var realPropertyName = mi.Name; 90 | 91 | property.ShouldSerialize = (o, value) => 92 | { 93 | var cp = o as IHaveChangedProperties; 94 | return cp?.ChangedProperties?.Contains(realPropertyName) == true; 95 | }; 96 | } 97 | }); 98 | 99 | 100 | return this; 101 | } 102 | 103 | 104 | #if NET6_0_OR_GREATER 105 | 106 | public JsonModelBuilder RespectNullableReferenceType() 107 | { 108 | var nullabilityInfoContext = new NullabilityInfoContext(); 109 | 110 | Configure(p => 111 | { 112 | foreach (var prop in p.Properties) 113 | { 114 | if (prop.Set is null || !prop.PropertyType.IsClass) 115 | continue; 116 | 117 | var nullState = prop.GetMemberInfo() switch 118 | { 119 | PropertyInfo pi => nullabilityInfoContext.Create(pi).WriteState, 120 | FieldInfo fi => nullabilityInfoContext.Create(fi).WriteState, 121 | _ => NullabilityState.Unknown 122 | }; 123 | 124 | if (nullState == NullabilityState.NotNull) 125 | { 126 | var set = prop.Set; 127 | var propertyName = prop.Name; 128 | prop.Set = (o, value) => 129 | { 130 | if (value is null) 131 | throw new JsonException($"Can not set null to none nullable property {propertyName}."); 132 | 133 | set(o, value); 134 | }; 135 | } 136 | } 137 | }); 138 | return this; 139 | } 140 | 141 | #endif 142 | public Action Build() 143 | { 144 | var config = _configurations.ToDictionary(p => p.Key, p => p.Value.Build()); 145 | var typeConfig = _typeConfigurations.ToArray(); 146 | return p => 147 | { 148 | foreach (var cfg in typeConfig) 149 | cfg(p); 150 | 151 | if (config.TryGetValue(p.Type, out var action)) 152 | action(p); 153 | }; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/JsonPropertyInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json.Serialization.Metadata; 3 | 4 | namespace SystemTextJson.FluentApi; 5 | 6 | internal static class JsonPropertyInfoExtensions 7 | { 8 | public static MemberInfo? GetMemberInfo(this JsonPropertyInfo jsonProp) => 9 | jsonProp.AttributeProvider as MemberInfo; 10 | } 11 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/JsonSerializerOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using System.Text.Json.Serialization.Metadata; 4 | 5 | namespace SystemTextJson.FluentApi; 6 | 7 | public static class JsonSerializerOptionsExtensions 8 | { 9 | public static JsonSerializerOptions ConfigureDefaultTypeResolver(this JsonSerializerOptions options, Action configureAction) 10 | { 11 | var modelBuilder = new JsonModelBuilder(); 12 | configureAction(modelBuilder); 13 | var action = modelBuilder.Build(); 14 | options.TypeInfoResolver ??= new DefaultJsonTypeInfoResolver(); 15 | options.TypeInfoResolver = options.TypeInfoResolver.WithAddedModifier(action); 16 | return options; 17 | } 18 | 19 | public static JsonSerializerOptions ConfigureEnumValues(this JsonSerializerOptions options, 20 | IReadOnlyDictionary? mapping = null, 21 | JsonNamingPolicy? namingPolicy = null, 22 | bool allowIntegerValues = true) 23 | where TEnum : struct, Enum 24 | { 25 | var innerConverterFactory = new JsonStringEnumConverter(namingPolicy, allowIntegerValues); 26 | var innerConverter = (JsonConverter)innerConverterFactory.CreateConverter(typeof(TEnum), options)!; 27 | 28 | JsonConverter converter = mapping is null ? 29 | new CustomEnumConverter(innerConverter, options) : 30 | new CustomEnumConverter(innerConverter, options, mapping); 31 | 32 | options.Converters.Add(converter); 33 | return options; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/JsonTypeInfoResolverExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization.Metadata; 3 | 4 | namespace SystemTextJson.FluentApi; 5 | 6 | public static class JsonTypeInfoResolverExtensions 7 | { 8 | public static IJsonTypeInfoResolver ConfigureTypes(this IJsonTypeInfoResolver resolver, Action configureAction) 9 | { 10 | var modelBuilder = new JsonModelBuilder(); 11 | configureAction(modelBuilder); 12 | var action = modelBuilder.Build(); 13 | return resolver.WithAddedModifier(action); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/MemberPropertyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | using System.Text.Json; 4 | using System.Text.Json.Nodes; 5 | using System.Text.Json.Serialization; 6 | using System.Text.Json.Serialization.Metadata; 7 | 8 | namespace SystemTextJson.FluentApi; 9 | 10 | public class MemberPropertyBuilder(MemberInfo memberInfo, IEntityTypeBuilder entityTypeBuilder) : IMemberPropertyBuilder, IPropertyBuilder 11 | { 12 | public string Name => MemberInfo.Name; 13 | public IList> JsonPropertyInfoActions { get; } = []; 14 | public MemberInfo MemberInfo { get; } = memberInfo; 15 | IEntityTypeBuilder IPropertyBuilder.EntityTypeBuilder => entityTypeBuilder; 16 | public IEntityTypeBuilder EntityTypeBuilder => entityTypeBuilder; 17 | 18 | public MemberPropertyBuilder IsExtensionData() 19 | { 20 | if (typeof(IDictionary).IsAssignableFrom(typeof(TProperty)) || 21 | typeof(IDictionary).IsAssignableFrom(typeof(TProperty)) || 22 | typeof(TProperty) == typeof(JsonObject)) 23 | { 24 | this.Configure(p => p.IsExtensionData = true); 25 | } 26 | else 27 | throw new InvalidOperationException($"The extension data property {typeof(TEntity)}{MemberInfo} is invalid. It must implement 'IDictionary' or 'IDictionary', or be 'JsonObject'."); 28 | 29 | return this; 30 | } 31 | 32 | public MemberPropertyBuilder IsPopulated() 33 | { 34 | this.Configure(p => p.ObjectCreationHandling = JsonObjectCreationHandling.Populate); 35 | return this; 36 | } 37 | 38 | public MemberPropertyBuilder IsRequired() 39 | { 40 | this.Configure(p => p.IsRequired = true); 41 | return this; 42 | } 43 | 44 | public MemberPropertyBuilder IsIgnored() // Add IsIgnoredIfNull/IfDefault 45 | { 46 | var mi = MemberInfo; 47 | 48 | entityTypeBuilder.Configure(p => 49 | { 50 | for (var i = 0; i < p.Properties.Count; i++) 51 | { 52 | if (p.Properties[i].GetMemberInfo() == mi) 53 | { 54 | p.Properties.RemoveAt(i); 55 | break; 56 | } 57 | } 58 | }); 59 | return this; 60 | } 61 | 62 | public MemberPropertyBuilder SerializeAsObject() => 63 | this.Configure(p => p.CustomConverter = ObjectSerializer.Instance); 64 | 65 | Action IPropertyBuilder.Build() 66 | { 67 | var configurations = JsonPropertyInfoActions.ToArray(); 68 | return p => 69 | { 70 | foreach (var cfg in configurations) 71 | cfg(p); 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/ObjectSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SystemTextJson.FluentApi; 5 | 6 | internal class ObjectSerializer : JsonConverter 7 | { 8 | public static JsonConverter Instance = new ObjectSerializer(); 9 | 10 | public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => 11 | throw new JsonException("Can not deserialize as object."); 12 | 13 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) 14 | { 15 | if (value is null) 16 | { 17 | writer.WriteNullValue(); 18 | } 19 | else 20 | { 21 | var objectConverter = options.GetConverter(value.GetType()); 22 | var mi = objectConverter.GetType().GetMethod(nameof(Write)) ?? throw new InvalidOperationException("JsonConverter was changed. Can't find Write method."); 23 | mi.Invoke(objectConverter, new object[] { writer, value, options }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/PropertyBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Text.Json.Serialization; 3 | using System.Text.Json.Serialization.Metadata; 4 | 5 | namespace SystemTextJson.FluentApi; 6 | 7 | public static class PropertyBuilderExtensions 8 | { 9 | public static T Configure(this T builder, Action configureAction) where T : IPropertyBuilder 10 | { 11 | builder.JsonPropertyInfoActions.Add(configureAction); 12 | return builder; 13 | } 14 | 15 | public static T HasName(this T builder, string name) where T : IPropertyBuilder => 16 | builder.Configure(p => p.Name = name); 17 | 18 | public static T HasConverter(this T builder, JsonConverter converter) where T : IPropertyBuilder => 19 | builder.Configure(p => p.CustomConverter = converter); 20 | 21 | public static T HasHumberHandling(this T builder, JsonNumberHandling numberHandling) where T : IPropertyBuilder => 22 | builder.Configure(p => p.NumberHandling = numberHandling); 23 | 24 | public static T HasOrder(this T builder, int order) where T : IPropertyBuilder => 25 | builder.Configure(p => p.Order = order); 26 | 27 | public static EntityTypeBuilder Entity(this IPropertyBuilder propertyBuilder) => 28 | propertyBuilder.EntityTypeBuilder.ModelBuilder.Entity(); 29 | 30 | public static MemberPropertyBuilder Property(this IPropertyBuilder builder, Expression> propertyExpression) => 31 | builder.EntityTypeBuilder.Property(propertyExpression); 32 | 33 | public static VirtualPropertyBuilder VirtualProperty(this IPropertyBuilder builder, string name, Func compute) => 34 | builder.EntityTypeBuilder.VirtualProperty(name, compute); 35 | } 36 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/SerializationHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.Json.Serialization; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | 9 | namespace SystemTextJson.FluentApi; 10 | internal class SerializationHelpers 11 | { 12 | public static T? ReadValue(JsonConverter converter, ref Utf8JsonReader reader, JsonSerializerOptions options) 13 | { 14 | var value = default(T); 15 | if (reader.TokenType == JsonTokenType.Null && !converter.HandleNull) 16 | { 17 | if (value is not null) 18 | throw new JsonException("Expected not null value."); 19 | } 20 | else 21 | { 22 | value = converter.Read(ref reader, typeof(T), options); 23 | } 24 | reader.Read(); 25 | return value; 26 | } 27 | 28 | public static void WriteValue(JsonConverter converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options) 29 | { 30 | if (value is null && !converter.HandleNull) 31 | writer.WriteNullValue(); 32 | else 33 | converter.Write(writer, value, options); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/SystemTextJson.FluentApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.0.1 4 | net8.0;net6.0;net7.0;netstandard2.0;net462 5 | enable 6 | enable 7 | 12 8 | true 9 | MIT 10 | True 11 | Fluent configuration library for System.Text.Json that allows developers to configure serialization uses strongly typed fluent interface and lambda expression. 12 | ilchert 13 | SystemTextJson.FluentApi is a fluent configuration library for System.Text.Json. 14 | https://github.com/Ilchert/SystemTextJson.FluentApi 15 | README.md 16 | https://github.com/Ilchert/SystemTextJson.FluentApi 17 | System.Text.Json; STJ; Serialization; JSON; Fluent Api 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/ValueTupleJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using static SystemTextJson.FluentApi.SerializationHelpers; 4 | namespace SystemTextJson.FluentApi; 5 | public class ValueTupleJsonConverter : JsonConverterFactory 6 | { 7 | public override bool CanConvert(Type typeToConvert) 8 | { 9 | return !typeToConvert.IsClass && 10 | typeToConvert.IsGenericType && 11 | typeToConvert.GetGenericTypeDefinition() is { } genericType && 12 | ( 13 | genericType == typeof(ValueTuple<>) || 14 | genericType == typeof(ValueTuple<,>) || 15 | genericType == typeof(ValueTuple<,,>) || 16 | genericType == typeof(ValueTuple<,,,>) || 17 | genericType == typeof(ValueTuple<,,,,>) || 18 | genericType == typeof(ValueTuple<,,,,,>) || 19 | genericType == typeof(ValueTuple<,,,,,,>) || 20 | genericType == typeof(ValueTuple<,,,,,,,>) || 21 | genericType == typeof(ValueTuple<,,,,,,,>) 22 | ); 23 | } 24 | 25 | public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) 26 | { 27 | var converterType = typeToConvert.GenericTypeArguments.Length switch 28 | { 29 | 1 => typeof(ValueTupleConverter<>), 30 | 2 => typeof(ValueTupleConverter<,>), 31 | 3 => typeof(ValueTupleConverter<,,>), 32 | 4 => typeof(ValueTupleConverter<,,,>), 33 | 5 => typeof(ValueTupleConverter<,,,,>), 34 | 6 => typeof(ValueTupleConverter<,,,,,>), 35 | 7 => typeof(ValueTupleConverter<,,,,,,>), 36 | 8 => typeof(ValueTupleConverter<,,,,,,,>), 37 | _ => throw new ArgumentOutOfRangeException(nameof(typeToConvert)) 38 | }; 39 | 40 | return (JsonConverter?)Activator.CreateInstance(converterType.MakeGenericType(typeToConvert.GenericTypeArguments), [options]); 41 | } 42 | 43 | private abstract class ValueTupleConverterBase : JsonConverter 44 | where TTuple : struct 45 | { 46 | public override TTuple Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 47 | { 48 | if (reader.TokenType != JsonTokenType.StartArray) 49 | throw new JsonException("Start token must be '['."); 50 | 51 | reader.Read(); 52 | 53 | var result = ReadTuple(ref reader, typeToConvert, options); 54 | 55 | if (reader.TokenType != JsonTokenType.EndArray) 56 | throw new JsonException("Expected end token ']'."); 57 | 58 | return result; 59 | } 60 | 61 | protected internal abstract TTuple ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options); 62 | 63 | public override void Write(Utf8JsonWriter writer, TTuple value, JsonSerializerOptions options) 64 | { 65 | writer.WriteStartArray(); 66 | WriteTuple(writer, value, options); 67 | writer.WriteEndArray(); 68 | } 69 | 70 | protected internal abstract void WriteTuple(Utf8JsonWriter writer, TTuple value, JsonSerializerOptions options); 71 | } 72 | 73 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase> 74 | { 75 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1)); 76 | protected internal override ValueTuple ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 77 | { 78 | var value1 = ReadValue(_converter1, ref reader, options); 79 | return ValueTuple.Create(value1); 80 | } 81 | 82 | protected internal override void WriteTuple(Utf8JsonWriter writer, ValueTuple value, JsonSerializerOptions options) 83 | { 84 | WriteValue(_converter1, writer, value.Item1, options); 85 | } 86 | } 87 | 88 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase> 89 | { 90 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1)); 91 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2)); 92 | 93 | protected internal override (T1?, T2?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 94 | { 95 | var value1 = ReadValue(_converter1, ref reader, options); 96 | var value2 = ReadValue(_converter2, ref reader, options); 97 | return (value1, value2); 98 | } 99 | 100 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?) value, JsonSerializerOptions options) 101 | { 102 | WriteValue(_converter1, writer, value.Item1, options); 103 | WriteValue(_converter2, writer, value.Item2, options); 104 | } 105 | } 106 | 107 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase> 108 | { 109 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1)); 110 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2)); 111 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3)); 112 | protected internal override (T1?, T2?, T3?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 113 | { 114 | var value1 = ReadValue(_converter1, ref reader, options); 115 | var value2 = ReadValue(_converter2, ref reader, options); 116 | var value3 = ReadValue(_converter3, ref reader, options); 117 | 118 | return (value1, value2, value3); 119 | } 120 | 121 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?, T3?) value, JsonSerializerOptions options) 122 | { 123 | WriteValue(_converter1, writer, value.Item1, options); 124 | WriteValue(_converter2, writer, value.Item2, options); 125 | WriteValue(_converter3, writer, value.Item3, options); 126 | } 127 | } 128 | 129 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase> 130 | { 131 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1)); 132 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2)); 133 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3)); 134 | private readonly JsonConverter _converter4 = (JsonConverter)options.GetConverter(typeof(T4)); 135 | 136 | protected internal override (T1?, T2?, T3?, T4?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 137 | { 138 | var value1 = ReadValue(_converter1, ref reader, options); 139 | var value2 = ReadValue(_converter2, ref reader, options); 140 | var value3 = ReadValue(_converter3, ref reader, options); 141 | var value4 = ReadValue(_converter4, ref reader, options); 142 | 143 | return (value1, value2, value3, value4); 144 | } 145 | 146 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?, T3?, T4?) value, JsonSerializerOptions options) 147 | { 148 | WriteValue(_converter1, writer, value.Item1, options); 149 | WriteValue(_converter2, writer, value.Item2, options); 150 | WriteValue(_converter3, writer, value.Item3, options); 151 | WriteValue(_converter4, writer, value.Item4, options); 152 | } 153 | } 154 | 155 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase> 156 | { 157 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1)); 158 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2)); 159 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3)); 160 | private readonly JsonConverter _converter4 = (JsonConverter)options.GetConverter(typeof(T4)); 161 | private readonly JsonConverter _converter5 = (JsonConverter)options.GetConverter(typeof(T5)); 162 | 163 | protected internal override (T1?, T2?, T3?, T4?, T5?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 164 | { 165 | var value1 = ReadValue(_converter1, ref reader, options); 166 | var value2 = ReadValue(_converter2, ref reader, options); 167 | var value3 = ReadValue(_converter3, ref reader, options); 168 | var value4 = ReadValue(_converter4, ref reader, options); 169 | var value5 = ReadValue(_converter5, ref reader, options); 170 | 171 | return (value1, value2, value3, value4, value5); 172 | } 173 | 174 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?, T3?, T4?, T5?) value, JsonSerializerOptions options) 175 | { 176 | WriteValue(_converter1, writer, value.Item1, options); 177 | WriteValue(_converter2, writer, value.Item2, options); 178 | WriteValue(_converter3, writer, value.Item3, options); 179 | WriteValue(_converter4, writer, value.Item4, options); 180 | WriteValue(_converter5, writer, value.Item5, options); 181 | } 182 | } 183 | 184 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase> 185 | { 186 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1)); 187 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2)); 188 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3)); 189 | private readonly JsonConverter _converter4 = (JsonConverter)options.GetConverter(typeof(T4)); 190 | private readonly JsonConverter _converter5 = (JsonConverter)options.GetConverter(typeof(T5)); 191 | private readonly JsonConverter _converter6 = (JsonConverter)options.GetConverter(typeof(T6)); 192 | 193 | protected internal override (T1?, T2?, T3?, T4?, T5?, T6?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 194 | { 195 | var value1 = ReadValue(_converter1, ref reader, options); 196 | var value2 = ReadValue(_converter2, ref reader, options); 197 | var value3 = ReadValue(_converter3, ref reader, options); 198 | var value4 = ReadValue(_converter4, ref reader, options); 199 | var value5 = ReadValue(_converter5, ref reader, options); 200 | var value6 = ReadValue(_converter6, ref reader, options); 201 | 202 | return (value1, value2, value3, value4, value5, value6); 203 | } 204 | 205 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?, T3?, T4?, T5?, T6?) value, JsonSerializerOptions options) 206 | { 207 | WriteValue(_converter1, writer, value.Item1, options); 208 | WriteValue(_converter2, writer, value.Item2, options); 209 | WriteValue(_converter3, writer, value.Item3, options); 210 | WriteValue(_converter4, writer, value.Item4, options); 211 | WriteValue(_converter5, writer, value.Item5, options); 212 | WriteValue(_converter6, writer, value.Item6, options); 213 | } 214 | } 215 | 216 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase> 217 | { 218 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1)); 219 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2)); 220 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3)); 221 | private readonly JsonConverter _converter4 = (JsonConverter)options.GetConverter(typeof(T4)); 222 | private readonly JsonConverter _converter5 = (JsonConverter)options.GetConverter(typeof(T5)); 223 | private readonly JsonConverter _converter6 = (JsonConverter)options.GetConverter(typeof(T6)); 224 | private readonly JsonConverter _converter7 = (JsonConverter)options.GetConverter(typeof(T7)); 225 | 226 | protected internal override (T1?, T2?, T3?, T4?, T5?, T6?, T7?) ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 227 | { 228 | var value1 = ReadValue(_converter1, ref reader, options); 229 | var value2 = ReadValue(_converter2, ref reader, options); 230 | var value3 = ReadValue(_converter3, ref reader, options); 231 | var value4 = ReadValue(_converter4, ref reader, options); 232 | var value5 = ReadValue(_converter5, ref reader, options); 233 | var value6 = ReadValue(_converter6, ref reader, options); 234 | var value7 = ReadValue(_converter7, ref reader, options); 235 | 236 | return (value1, value2, value3, value4, value5, value6, value7); 237 | } 238 | 239 | protected internal override void WriteTuple(Utf8JsonWriter writer, (T1?, T2?, T3?, T4?, T5?, T6?, T7?) value, JsonSerializerOptions options) 240 | { 241 | WriteValue(_converter1, writer, value.Item1, options); 242 | WriteValue(_converter2, writer, value.Item2, options); 243 | WriteValue(_converter3, writer, value.Item3, options); 244 | WriteValue(_converter4, writer, value.Item4, options); 245 | WriteValue(_converter5, writer, value.Item5, options); 246 | WriteValue(_converter6, writer, value.Item6, options); 247 | WriteValue(_converter7, writer, value.Item7, options); 248 | } 249 | } 250 | 251 | private class ValueTupleConverter(JsonSerializerOptions options) : ValueTupleConverterBase> 252 | where TRest : struct 253 | { 254 | private readonly JsonConverter _converter1 = (JsonConverter)options.GetConverter(typeof(T1)); 255 | private readonly JsonConverter _converter2 = (JsonConverter)options.GetConverter(typeof(T2)); 256 | private readonly JsonConverter _converter3 = (JsonConverter)options.GetConverter(typeof(T3)); 257 | private readonly JsonConverter _converter4 = (JsonConverter)options.GetConverter(typeof(T4)); 258 | private readonly JsonConverter _converter5 = (JsonConverter)options.GetConverter(typeof(T5)); 259 | private readonly JsonConverter _converter6 = (JsonConverter)options.GetConverter(typeof(T6)); 260 | private readonly JsonConverter _converter7 = (JsonConverter)options.GetConverter(typeof(T7)); 261 | private readonly ValueTupleConverterBase _converterRest = (ValueTupleConverterBase)options.GetConverter(typeof(TRest)); 262 | 263 | protected internal override ValueTuple ReadTuple(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 264 | { 265 | var value1 = ReadValue(_converter1, ref reader, options); 266 | var value2 = ReadValue(_converter2, ref reader, options); 267 | var value3 = ReadValue(_converter3, ref reader, options); 268 | var value4 = ReadValue(_converter4, ref reader, options); 269 | var value5 = ReadValue(_converter5, ref reader, options); 270 | var value6 = ReadValue(_converter6, ref reader, options); 271 | var value7 = ReadValue(_converter7, ref reader, options); 272 | 273 | var restValue = _converterRest.ReadTuple(ref reader, typeof(TRest), options); 274 | 275 | return new ValueTuple(value1, value2, value3, value4, value5, value6, value7, restValue); 276 | } 277 | 278 | protected internal override void WriteTuple(Utf8JsonWriter writer, ValueTuple value, JsonSerializerOptions options) 279 | { 280 | WriteValue(_converter1, writer, value.Item1, options); 281 | WriteValue(_converter2, writer, value.Item2, options); 282 | WriteValue(_converter3, writer, value.Item3, options); 283 | WriteValue(_converter4, writer, value.Item4, options); 284 | WriteValue(_converter5, writer, value.Item5, options); 285 | WriteValue(_converter6, writer, value.Item6, options); 286 | WriteValue(_converter7, writer, value.Item7, options); 287 | 288 | _converterRest.WriteTuple(writer, value.Rest, options); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/SystemTextJson.FluentApi/VirtualPropertyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization.Metadata; 2 | 3 | namespace SystemTextJson.FluentApi; 4 | public class VirtualPropertyBuilder(string name, IEntityTypeBuilder entityTypeBuilder) : IPropertyBuilder 5 | { 6 | public IEntityTypeBuilder EntityTypeBuilder => entityTypeBuilder; 7 | 8 | IEntityTypeBuilder IPropertyBuilder.EntityTypeBuilder => entityTypeBuilder; 9 | 10 | public string Name => name; 11 | 12 | public IList> JsonPropertyInfoActions { get; } = []; 13 | 14 | Action IPropertyBuilder.Build() 15 | { 16 | var configurations = JsonPropertyInfoActions.ToArray(); 17 | return p => 18 | { 19 | foreach (var cfg in configurations) 20 | cfg(p); 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/SystemTextJson.FluentApi.Tests/CustomizableJsonStringEnumConverterTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | using System.Text.Json.Serialization; 4 | using Xunit; 5 | 6 | namespace SystemTextJson.FluentApi.Tests; 7 | 8 | public class CustomizableJsonStringEnumConverterTests 9 | { 10 | readonly JsonSerializerOptions _optionsWithGlobalSettings = new JsonSerializerOptions() { Converters = { new CustomizableJsonStringEnumConverter(JsonNamingPolicy.CamelCase) } }; 11 | 12 | 13 | [Theory] 14 | [InlineData(A.First, "\"f\"")] 15 | [InlineData(null, "null")] 16 | [InlineData(A.Third, "\"third\"")] 17 | [InlineData((A)8, "8")] 18 | public void WriteGlobal(A? value, string json) 19 | { 20 | var actual = JsonSerializer.Serialize([value], _optionsWithGlobalSettings); 21 | Assert.True(JsonNode.DeepEquals($"""[{json}]""", actual)); 22 | 23 | } 24 | 25 | [Theory] 26 | [InlineData(A.First, "\"f\"")] 27 | [InlineData(null, "null")] 28 | [InlineData(A.Third, "\"third\"")] 29 | [InlineData((A)8, "8")] 30 | public void ReadGlobal(A? value, string json) 31 | { 32 | var actual = JsonSerializer.Deserialize($"""[{json}]""", _optionsWithFluentSettings); 33 | Assert.Equal([value], actual); 34 | } 35 | 36 | readonly JsonSerializerOptions _optionsWithFluentSettings = new JsonSerializerOptions().ConfigureEnumValues(namingPolicy: JsonNamingPolicy.CamelCase); 37 | 38 | [Theory] 39 | [InlineData(A.First, "\"f\"")] 40 | [InlineData(null, "null")] 41 | [InlineData(A.Third, "\"third\"")] 42 | [InlineData((A)8, "8")] 43 | public void WriteFluentDefault(A? value, string json) 44 | { 45 | var actual = JsonSerializer.Serialize([value], _optionsWithFluentSettings); 46 | Assert.True(JsonNode.DeepEquals($"""[{json}]""", actual)); 47 | 48 | } 49 | 50 | [Theory] 51 | [InlineData(A.First, "\"f\"")] 52 | [InlineData(null, "null")] 53 | [InlineData(A.Third, "\"third\"")] 54 | [InlineData((A)8, "8")] 55 | public void ReadFluentDefault(A? value, string json) 56 | { 57 | var actual = JsonSerializer.Deserialize($"""[{json}]""", _optionsWithGlobalSettings); 58 | Assert.Equal([value], actual); 59 | } 60 | 61 | 62 | readonly JsonSerializerOptions _optionsWithFluentMapping = new JsonSerializerOptions().ConfigureEnumValues( 63 | new Dictionary { { A.First, "f1" } }, 64 | namingPolicy: JsonNamingPolicy.CamelCase); 65 | 66 | [Theory] 67 | [InlineData(A.First, "\"f1\"")] 68 | [InlineData(null, "null")] 69 | [InlineData(A.Third, "\"third\"")] 70 | [InlineData((A)8, "8")] 71 | public void WriteFluentMapping(A? value, string json) 72 | { 73 | var actual = JsonSerializer.Serialize([value], _optionsWithFluentMapping); 74 | Assert.True(JsonNode.DeepEquals($"""[{json}]""", actual)); 75 | 76 | } 77 | 78 | [Theory] 79 | [InlineData(A.First, "\"f1\"")] 80 | [InlineData(null, "null")] 81 | [InlineData(A.Third, "\"third\"")] 82 | [InlineData((A)8, "8")] 83 | public void ReadFluentMapping(A? value, string json) 84 | { 85 | var actual = JsonSerializer.Deserialize($"""[{json}]""", _optionsWithFluentMapping); 86 | Assert.Equal([value], actual); 87 | } 88 | 89 | 90 | public enum A 91 | { 92 | None, 93 | [JsonPropertyName("f")] 94 | First = 1, 95 | Second = 2, 96 | Third = 3 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /tests/SystemTextJson.FluentApi.Tests/EntityTypeBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization.Metadata; 2 | using System.Text.Json; 3 | using Xunit; 4 | using System.Text.Json.Serialization; 5 | using System.Reflection; 6 | 7 | namespace SystemTextJson.FluentApi.Tests; 8 | 9 | public class EntityTypeBuilderTests 10 | { 11 | private readonly JsonSerializerOptions _options; 12 | public EntityTypeBuilderTests() 13 | { 14 | _options = new JsonSerializerOptions() 15 | { 16 | TypeInfoResolver = new DefaultJsonTypeInfoResolver(), 17 | IncludeFields = true 18 | }; 19 | } 20 | 21 | [Fact] 22 | public void IsUnmappedMemberDisallowed() 23 | { 24 | _options.ConfigureDefaultTypeResolver(builder => 25 | builder.Entity() 26 | .IsUnmappedMemberDisallowed()); 27 | 28 | Assert.ThrowsAny(() => JsonSerializer.Deserialize("""{"UnmappedProperty": null}""", _options)); 29 | } 30 | 31 | [Fact] 32 | public void VirtualProperty() 33 | { 34 | _options.ConfigureDefaultTypeResolver(builder => 35 | builder.Entity() 36 | .Property(p => p.Property).IsIgnored() 37 | .Property(p => p.Field).IsIgnored() 38 | .VirtualProperty("virtualProperty", p => "computed") 39 | .Entity() 40 | .VirtualProperty("virtualProperty", p => "computed") 41 | .HasName("renamedVirtualProperty")); 42 | 43 | JsonAsserts.AssertJson(new TestClass { }, """{"renamedVirtualProperty":"computed"}""", _options); 44 | } 45 | 46 | [Fact] 47 | public void HasDerivedType() 48 | { 49 | _options.ConfigureDefaultTypeResolver(builder => 50 | builder.Entity() 51 | .HasDerivedType(nameof(Derived1)) 52 | .HasDerivedType(nameof(Derived2)) 53 | .HasDerivedType(nameof(Root))); 54 | 55 | var testObject = new Root[] 56 | { 57 | new Derived1() { Derived1Property = "derived" }, 58 | new Derived2() { Derived2Property = "derived2" }, 59 | new Root(){ RootProperty = "root"} 60 | }; 61 | JsonAsserts.AssertJsonAndObject(testObject, """ 62 | [ 63 | {"$type":"Derived1","Derived1Property":"derived","RootProperty":null}, 64 | {"$type":"Derived2","Derived2Property":"derived2","RootProperty":null}, 65 | {"$type":"Root","RootProperty":"root"}] 66 | """, _options); 67 | } 68 | 69 | 70 | [Fact] 71 | public void HasDerivedTypesFromAssembly() 72 | { 73 | _options.ConfigureDefaultTypeResolver(builder => 74 | builder.Entity().HasDerivedTypesFromAssembly(Assembly.GetExecutingAssembly(), t => t.Name)); 75 | 76 | var testObject = new Root[] 77 | { 78 | new Derived1() { Derived1Property = "derived" }, 79 | new Derived2() { Derived2Property = "derived2" }, 80 | new Root(){ RootProperty = "root"} 81 | }; 82 | JsonAsserts.AssertJsonAndObject(testObject, """ 83 | [ 84 | {"$type":"Derived1","Derived1Property":"derived","RootProperty":null}, 85 | {"$type":"Derived2","Derived2Property":"derived2","RootProperty":null}, 86 | {"$type":"Root","RootProperty":"root"}] 87 | """, _options); 88 | } 89 | 90 | 91 | public class Root 92 | { 93 | public string? RootProperty { get; set; } 94 | } 95 | 96 | public class Derived1 : Root 97 | { 98 | public string? Derived1Property { get; set; } 99 | } 100 | 101 | public class Derived2 : Root 102 | { 103 | public string? Derived2Property { get; set; } 104 | } 105 | 106 | public class TestClass 107 | { 108 | [JsonPropertyName("Pro")] 109 | public string? Property { get; set; } 110 | 111 | public string? Field; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/SystemTextJson.FluentApi.Tests/InlineArrayJsonConverterTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using System.Runtime.CompilerServices; 3 | using System.Text.Json; 4 | using System.Text.Json.Nodes; 5 | 6 | namespace SystemTextJson.FluentApi.Tests; 7 | public class InlineArrayJsonConverterTests 8 | { 9 | readonly JsonSerializerOptions _options = new() { Converters = { new InlineArrayJsonConverter() } }; 10 | 11 | [Fact] 12 | public void Write() 13 | { 14 | var array = new InlineArray(); 15 | array[0] = null; 16 | array[1] = 1; 17 | array[2] = -1; 18 | 19 | var actualJson = JsonSerializer.Serialize(array, _options); 20 | 21 | var isEquals = JsonNode.DeepEquals(JsonNode.Parse("[null,1,-1]"), JsonNode.Parse(actualJson)); 22 | Assert.True(isEquals, "Json not equal."); 23 | } 24 | 25 | [Fact] 26 | public void WriteDto() 27 | { 28 | var array = new InlineArray(); 29 | array[0] = null; 30 | array[1] = 1; 31 | array[2] = -1; 32 | var dto = new Dto() { Array = array }; 33 | 34 | var actualJson = JsonSerializer.Serialize(dto, _options); 35 | 36 | var isEquals = JsonNode.DeepEquals(JsonNode.Parse("{\"Array\":[null,1,-1]}"), JsonNode.Parse(actualJson)); 37 | Assert.True(isEquals, "Json not equal."); 38 | } 39 | 40 | [Fact] 41 | public void Read() 42 | { 43 | var actual = JsonSerializer.Deserialize("[null,1,-1]", _options); 44 | 45 | Assert.Null(actual[0]); 46 | Assert.Equal(actual[1], 1); 47 | Assert.Equal(actual[2], -1); 48 | } 49 | 50 | 51 | [Fact] 52 | public void ReadDto() 53 | { 54 | var actual = JsonSerializer.Deserialize("{\"Array\":[null,1,-1]}", _options); 55 | 56 | Assert.NotNull(actual); 57 | var array = Assert.NotNull(actual.Array); 58 | Assert.Null(array[0]); 59 | Assert.Equal(array[1], 1); 60 | Assert.Equal(array[2], -1); 61 | } 62 | 63 | private class Dto 64 | { 65 | public InlineArray? Array { get; set; } 66 | } 67 | 68 | [InlineArray(3)] 69 | private struct InlineArray 70 | { 71 | public int? Value; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/SystemTextJson.FluentApi.Tests/JsonAsserts.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | using Xunit; 5 | 6 | namespace SystemTextJson.FluentApi.Tests; 7 | 8 | public static class JsonAsserts 9 | { 10 | public static void AssertJsonAndObject(T testObject, [StringSyntax(StringSyntaxAttribute.Json)] string expectedJson, JsonSerializerOptions options) 11 | { 12 | AssertJson(testObject, expectedJson, options); 13 | AssertObject(testObject, expectedJson, options); 14 | } 15 | 16 | public static void AssertJson(T testObject, [StringSyntax(StringSyntaxAttribute.Json)] string expectedJson, JsonSerializerOptions options) 17 | { 18 | var json = JsonSerializer.Serialize(testObject, options); 19 | var actual = JsonNode.Parse(json); 20 | var expected = JsonNode.Parse(expectedJson); 21 | Assert.True(JsonNode.DeepEquals(expected, actual), "Jsons are not equals."); 22 | } 23 | 24 | public static void AssertObject(T testObject, [StringSyntax(StringSyntaxAttribute.Json)] string expectedJson, JsonSerializerOptions options) 25 | { 26 | var expectedObject = JsonSerializer.Deserialize(expectedJson, options); 27 | Assert.Equivalent(testObject, expectedObject); 28 | } 29 | } -------------------------------------------------------------------------------- /tests/SystemTextJson.FluentApi.Tests/JsonModelBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using System.Text.Json.Serialization.Metadata; 4 | using Xunit; 5 | 6 | namespace SystemTextJson.FluentApi.Tests; 7 | 8 | public class JsonModelBuilderTests 9 | { 10 | private readonly JsonSerializerOptions _options; 11 | public JsonModelBuilderTests() 12 | { 13 | _options = new JsonSerializerOptions() 14 | { 15 | TypeInfoResolver = new DefaultJsonTypeInfoResolver(), 16 | IncludeFields = true 17 | }; 18 | } 19 | 20 | 21 | [Fact] 22 | public void MultiConfiguration() 23 | { 24 | _options.ConfigureDefaultTypeResolver(builder => 25 | builder.Entity() 26 | .Property(p => p.Property).HasName("Prop1") 27 | .Property(p => p.Property).HasName("Prop2") 28 | .Entity() 29 | .Property(p => p.Field).HasName("Field1").HasName("Field2")); 30 | 31 | JsonAsserts.AssertJsonAndObject(new TestClass() { Property = "Prop", Field = "Field" }, """{"Prop2":"Prop","Field2":"Field"}""", _options); 32 | } 33 | 34 | [Fact] 35 | public void RespectNullableReferenceType() 36 | { 37 | _options.ConfigureDefaultTypeResolver(builder => 38 | builder.RespectNullableReferenceType()); 39 | 40 | JsonAsserts.AssertObject(new TestClass(), "{}", _options); 41 | 42 | Assert.ThrowsAny(() => JsonSerializer.Deserialize("""{"Pro": null}""", _options)); 43 | Assert.ThrowsAny(() => JsonSerializer.Deserialize("""{"Field": null}""", _options)); 44 | JsonAsserts.AssertObject(new TestClass(), "{}", _options); 45 | } 46 | 47 | [Fact] 48 | public void TrackChangedProperties() 49 | { 50 | _options.ConfigureDefaultTypeResolver(builder => builder.TrackChangedProperties()); 51 | 52 | var obj = JsonSerializer.Deserialize("""{"StringProperty":"str", "IntProperty":1}""", _options); 53 | 54 | Assert.NotNull(obj); 55 | Assert.Contains(nameof(TrackTestClass.IntProperty), obj.ChangedProperties); 56 | Assert.Contains(nameof(TrackTestClass.StringProperty), obj.ChangedProperties); 57 | } 58 | 59 | [Fact] 60 | public void SerializeOnlyChangedProperties() 61 | { 62 | _options.ConfigureDefaultTypeResolver(builder => builder.SerializeOnlyChangedProperties()); 63 | 64 | var testObject = new TrackTestClass() 65 | { 66 | StringProperty = "str", 67 | IntProperty = 1, 68 | ChangedProperties = { nameof(TrackTestClass.IntProperty) } 69 | }; 70 | 71 | JsonAsserts.AssertJson(testObject, """{"IntProperty":1}""", _options); 72 | } 73 | 74 | public class TrackTestClass : IHaveChangedProperties 75 | { 76 | public string? StringProperty { get; set; } 77 | public int IntProperty { get; set; } 78 | public ISet ChangedProperties { get; } = new HashSet(); 79 | } 80 | 81 | public class TestClass 82 | { 83 | [JsonPropertyName("Pro")] 84 | public string Property { get; set; } = null!; 85 | 86 | public string Field = null!; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/SystemTextJson.FluentApi.Tests/PropertyBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Nodes; 3 | using System.Text.Json.Serialization; 4 | using System.Text.Json.Serialization.Metadata; 5 | using Xunit; 6 | 7 | namespace SystemTextJson.FluentApi.Tests; 8 | 9 | public class PropertyBuilderTests 10 | { 11 | private readonly JsonSerializerOptions _options; 12 | 13 | public PropertyBuilderTests() 14 | { 15 | _options = new JsonSerializerOptions() 16 | { 17 | TypeInfoResolver = new DefaultJsonTypeInfoResolver(), 18 | IncludeFields = true 19 | }; 20 | } 21 | 22 | [Fact] 23 | public void HasName() 24 | { 25 | 26 | _options.ConfigureDefaultTypeResolver(builder => 27 | builder.Entity() 28 | .Property(p => p.Property).HasName("PropertyName") 29 | .Property(p => p.Field).HasName("FieldName")); 30 | 31 | var testObject = new TestClass { Property = "Prop", Field = "field" }; 32 | 33 | JsonAsserts.AssertJsonAndObject(testObject, """{"PropertyName":"Prop","FieldName":"field"}""", _options); 34 | } 35 | 36 | [Fact] 37 | public void HasConverter() 38 | { 39 | var converter = new TestConverter(); 40 | 41 | _options.ConfigureDefaultTypeResolver(builder => 42 | builder.Entity() 43 | .Property(p => p.Property).HasConverter(converter) 44 | .Property(p => p.Field).HasConverter(converter)); 45 | 46 | var testObject = new TestClass { Property = "Prop", Field = "field" }; 47 | 48 | JsonAsserts.AssertJsonAndObject(testObject, """{"Property":"Prop","Field":"field"}""", _options); 49 | 50 | Assert.Equal(4, converter.CallCount); 51 | } 52 | 53 | [Fact] 54 | public void IsExtensionData_Object() 55 | { 56 | _options.ConfigureDefaultTypeResolver(builder => 57 | builder.Entity() 58 | .Property(p => p.Data).IsExtensionData()); 59 | 60 | 61 | var testObject = new ExtensionDataObject 62 | { 63 | Data = new() 64 | { 65 | { "Property", "Prop" }, 66 | { "Field", "field" }, 67 | } 68 | }; 69 | var json = """{"Property":"Prop","Field":"field"}"""; 70 | JsonAsserts.AssertJson(testObject, json, _options); 71 | var deserializedObject = JsonSerializer.Deserialize(json, _options); 72 | Assert.NotNull(deserializedObject); 73 | Assert.NotNull(deserializedObject.Data); 74 | Assert.Contains("Property", deserializedObject.Data); 75 | Assert.Contains("Field", deserializedObject.Data); 76 | } 77 | 78 | [Fact] 79 | public void IsExtensionData_JsonElement() 80 | { 81 | _options.ConfigureDefaultTypeResolver(builder => 82 | builder.Entity() 83 | .Property(p => p.Data).IsExtensionData()); 84 | 85 | var json = """{"Property":"Prop","Field":"field"}"""; 86 | var deserializedObject = JsonSerializer.Deserialize(json, _options); 87 | Assert.NotNull(deserializedObject); 88 | Assert.NotNull(deserializedObject.Data); 89 | Assert.Contains("Property", deserializedObject.Data); 90 | Assert.Contains("Field", deserializedObject.Data); 91 | 92 | JsonAsserts.AssertJson(deserializedObject, json, _options); 93 | } 94 | 95 | [Fact] 96 | public void IsExtensionData_JsonObject() 97 | { 98 | _options.ConfigureDefaultTypeResolver(builder => 99 | builder.Entity() 100 | .Property(p => p.Data).IsExtensionData()); 101 | 102 | var json = """{"Property":"Prop","Field":"field"}"""; 103 | var deserializedObject = JsonSerializer.Deserialize(json, _options); 104 | Assert.NotNull(deserializedObject); 105 | Assert.NotNull(deserializedObject.Data); 106 | Assert.Contains("Property", deserializedObject.Data); 107 | Assert.Contains("Field", deserializedObject.Data); 108 | 109 | Assert.ThrowsAny(() => JsonAsserts.AssertJson(deserializedObject, json, _options));// https://github.com/dotnet/runtime/issues/60560 110 | } 111 | 112 | [Fact] 113 | public void IsExtensionData_JsonNode() 114 | { 115 | Assert.Throws(() => 116 | _options.ConfigureDefaultTypeResolver(builder => 117 | builder.Entity() 118 | .Property(p => p.Data).IsExtensionData())); 119 | } 120 | 121 | [Fact] 122 | public void HasNumberHandling() 123 | { 124 | _options.ConfigureDefaultTypeResolver(builder => 125 | builder.Entity() 126 | .Property(p => p.Number).HasHumberHandling(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString)); 127 | 128 | var testObject = new NumberHandlinClass { Number = 1 }; 129 | 130 | JsonAsserts.AssertJsonAndObject(testObject, """{"Number":"1"}""", _options); 131 | } 132 | 133 | [Fact] 134 | public void HasObjectCreationHandling() 135 | { 136 | _options.ConfigureDefaultTypeResolver(builder => 137 | builder.Entity() 138 | .Property(p => p.Prop).IsPopulated()); 139 | 140 | var testObject = new ObjectCreationHandlingClass(); 141 | JsonAsserts.AssertObject(testObject, """{"Prop":{ }}""", _options); 142 | } 143 | 144 | [Fact] 145 | public void HasOrder() 146 | { 147 | _options.ConfigureDefaultTypeResolver(builder => 148 | builder.Entity() 149 | .Property(p => p.Property).HasOrder(2) 150 | .Property(p => p.Field).HasOrder(1)); 151 | 152 | var testObject = new TestClass { Property = "Prop", Field = "field" }; 153 | var json = JsonSerializer.Serialize(testObject, _options); 154 | using var doc = JsonDocument.Parse(json); 155 | Assert.Collection(doc.RootElement.EnumerateObject(), 156 | p => Assert.Equal("Field", p.Name), 157 | p => Assert.Equal("Property", p.Name)); 158 | } 159 | 160 | 161 | [Fact] 162 | public void IsRequired() 163 | { 164 | _options.ConfigureDefaultTypeResolver(builder => 165 | builder.Entity() 166 | .Property(p => p.Property).IsRequired()); 167 | 168 | Assert.ThrowsAny(() => JsonSerializer.Deserialize("{}", _options)); 169 | } 170 | 171 | [Fact] 172 | public void Ignore() 173 | { 174 | _options.TypeInfoResolver = _options.TypeInfoResolver! 175 | .ConfigureTypes(builder => 176 | builder.Entity() 177 | .Property(p => p.Property).IsIgnored() 178 | .Property(p => p.Field).IsIgnored()); 179 | 180 | var testObject = new TestClass { Property = "Prop", Field = "field" }; 181 | 182 | JsonAsserts.AssertJson(testObject, """{}""", _options); 183 | JsonAsserts.AssertObject(new TestClass { }, """{"Property":"Prop","Field":"field"}""", _options); 184 | } 185 | 186 | [Fact] 187 | public void SerializeAsObject() 188 | { 189 | _options.ConfigureDefaultTypeResolver(builder => 190 | builder.Entity() 191 | .Property(p => p.Data).SerializeAsObject()); 192 | 193 | var testObject = new AsObjectTestClass { Data = new Derived() { Property = "Prop" } }; 194 | JsonSerializer.Serialize(testObject, _options); 195 | JsonAsserts.AssertJson(testObject, """{"Data":{"Property":"Prop"}}""", _options); 196 | Assert.Throws(() => JsonAsserts.AssertObject(new AsObjectTestClass(), """{"Data":{"Property":"Prop"}}""", _options)); 197 | } 198 | 199 | private class TestConverter : JsonConverter 200 | { 201 | public int CallCount { get; private set; } 202 | public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 203 | { 204 | CallCount++; 205 | return reader.GetString(); 206 | } 207 | 208 | public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) 209 | { 210 | CallCount++; 211 | writer.WriteStringValue(value); 212 | } 213 | } 214 | 215 | public class ExtensionDataObject 216 | { 217 | public Dictionary? Data { get; set; } 218 | } 219 | 220 | public class ExtensionDataJsonElement 221 | { 222 | public Dictionary? Data { get; set; } 223 | } 224 | 225 | public class ExtensionDataJsonNode 226 | { 227 | public Dictionary? Data { get; set; } 228 | } 229 | 230 | public class ExtensionDataJsonObject 231 | { 232 | public JsonObject? Data { get; set; } 233 | } 234 | 235 | public class TestClass 236 | { 237 | public string? Property { get; set; } 238 | 239 | public string? Field; 240 | } 241 | 242 | public class NumberHandlinClass 243 | { 244 | public int? Number { get; set; } 245 | } 246 | 247 | public class ObjectCreationHandlingClass 248 | { 249 | public TestClass Prop { get; set; } = new TestClass() { Property = "1" }; 250 | } 251 | 252 | public class AsObjectTestClass 253 | { 254 | public Root? Data { get; set; } 255 | } 256 | 257 | public class Root { } 258 | 259 | public class Derived : Root 260 | { 261 | public string? Property { get; set; } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /tests/SystemTextJson.FluentApi.Tests/SystemTextJson.FluentApi.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/SystemTextJson.FluentApi.Tests/ValueTupleJsonConverterTests.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | using Xunit; 5 | 6 | namespace SystemTextJson.FluentApi.Tests; 7 | public class ValueTupleJsonConverterTests 8 | { 9 | private JsonSerializerOptions _options = new JsonSerializerOptions() { Converters = { new ValueTupleJsonConverter() } }; 10 | 11 | 12 | public static IEnumerable Data() 13 | { 14 | (object? value, Type type, string json)[] initialData = [ 15 | (1, typeof(int),"1"), 16 | (null,typeof(int?),"null"), 17 | (2.2, typeof(double),"2.2"), 18 | (null, typeof(string),"null"), 19 | ("",typeof(string),"\"\""), 20 | ("str", typeof(string),"\"str\""), 21 | (null,typeof(int[]),"null"), 22 | (Array.Empty(),typeof(int[]),"[]"), 23 | (new int[]{1,2},typeof(int[]),"[1,2]"), 24 | (null,typeof(EmptyDto),"null"), 25 | (new EmptyDto(),typeof(EmptyDto),"{}"), 26 | ]; 27 | 28 | var defaultItem = initialData[0]; 29 | 30 | for (var i = 1; i < 9; i++) 31 | { 32 | var template = Enumerable.Repeat(defaultItem, i).ToArray(); 33 | for (var j = 0; j < template.Length; j++) 34 | { 35 | foreach (var item in initialData) 36 | { 37 | template[j] = item; 38 | var tuple = CreateTuple(template.Select(p => p.type).ToArray(), template.Select(p => p.value).ToArray()); 39 | var json = string.Join(",", template.Select(p => p.json)); 40 | yield return [tuple, $"[{json}]"]; 41 | } 42 | template[j] = defaultItem; 43 | } 44 | } 45 | 46 | yield return [(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17), "[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]"]; 47 | } 48 | 49 | private static object CreateTuple(Type[] types, object?[] values) 50 | { 51 | var m = typeof(ValueTuple).GetMethods(BindingFlags.Static | BindingFlags.Public) 52 | .Single(p => p.GetParameters().Length == types.Length); 53 | var genericMethod = m.MakeGenericMethod(types); 54 | return genericMethod.Invoke(null, values)!; 55 | } 56 | 57 | 58 | [Theory] 59 | [MemberData(nameof(Data))] 60 | public void ReadJson(T expected, string json) 61 | { 62 | var actual = JsonSerializer.Deserialize(json, _options); 63 | Assert.Equivalent(expected, actual); 64 | } 65 | 66 | [Theory] 67 | [MemberData(nameof(Data))] 68 | public void WriteJson(T value, string expected) 69 | { 70 | var actual = JsonSerializer.Serialize(value, _options); 71 | var isEquals = JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)); 72 | Assert.True(isEquals, "Json not equal."); 73 | } 74 | 75 | [Theory] 76 | [MemberData(nameof(Data))] 77 | public void ReadJsonFromDto(T expected, string json) 78 | { 79 | var actual = JsonSerializer.Deserialize>($$"""{"A":{{json}}, "B":"123"}""", _options); 80 | Assert.Equivalent(new Dto { A = expected, B = "123" }, actual); 81 | } 82 | 83 | [Theory] 84 | [MemberData(nameof(Data))] 85 | public void WriteJsonFromDto(T value, string expected) 86 | { 87 | var actual = JsonSerializer.Serialize(new Dto { A = value, B = "123" }, _options); 88 | var isEquals = JsonNode.DeepEquals(JsonNode.Parse($$"""{"A":{{expected}}, "B":"123"}"""), JsonNode.Parse(actual)); 89 | Assert.True(isEquals, "Json not equal."); 90 | } 91 | 92 | [Theory] 93 | [MemberData(nameof(Data))] 94 | public void ReadJsonFromArray(T expected, string json) 95 | { 96 | var actual = JsonSerializer.Deserialize($"[{json}]", _options); 97 | Assert.Equivalent((T[])[expected], actual); 98 | } 99 | 100 | [Theory] 101 | [MemberData(nameof(Data))] 102 | public void WriteJsonFromArray(T value, string expected) 103 | { 104 | var actual = JsonSerializer.Serialize([value], _options); 105 | var isEquals = JsonNode.DeepEquals(JsonNode.Parse($"[{expected}]"), JsonNode.Parse(actual)); 106 | Assert.True(isEquals, "Json not equal."); 107 | } 108 | 109 | private class EmptyDto(); 110 | 111 | private class Dto 112 | { 113 | public T? A { get; set; } 114 | public string? B { get; set; } 115 | } 116 | } 117 | --------------------------------------------------------------------------------