diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 133cdf1..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: 2.1 - -jobs: - build-test: - docker: - - image: microsoft/dotnet:latest - steps: - - checkout - - run: - working_directory: ~/project - name: Build and run tests - command: | - dotnet test FSharp.Json.Tests - release: - docker: - - image: microsoft/dotnet:latest - steps: - - checkout - - run: - working_directory: ~/project - name: Pack and publish to NuGet - command: | - dotnet pack FSharp.Json --configuration Release - dotnet nuget push ./FSharp.Json/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key $NUGET_KEY - -workflows: - build-test: - jobs: - - build-test: - filters: - tags: - only: /^v.*/ - - release: - context: nuget - requires: - - build-test - filters: - tags: - only: /^v.*/ - branches: - ignore: /.*/ diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..fc21e63 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "paket": { + "version": "6.2.1", + "commands": [ + "paket" + ] + }, + "fantomas-tool": { + "version": "4.6.0-alpha-011", + "commands": [ + "fantomas" + ] + } + } +} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..dcd6cf9 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,5 @@ +FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-5.0 + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..ac73177 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,52 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/dotnet +{ + "name": "F# (.NET)", + "build": { + "dockerfile": "Dockerfile", + "args": {} + }, + // Set *default* container specific settings.json values on container create. + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "/bin/bash" + }, + } + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "editorconfig.editorconfig", + "ms-dotnettools.csharp", + "ionide.ionide-fsharp", + ], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [5000, 5001], + // [Optional] To reuse of your local HTTPS dev cert: + // + // 1. Export it locally using this command: + // * Windows PowerShell: + // dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" + // * macOS/Linux terminal: + // dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" + // + // 2. Uncomment these 'remoteEnv' lines: + // "remoteEnv": { + // "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere", + // "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx", + // }, + // + // 3. Do one of the following depending on your scenario: + // * When using GitHub Codespaces and/or Remote - Containers: + // 1. Start the container + // 2. Drag ~/.aspnet/https/aspnetapp.pfx into the root of the file explorer + // 3. Open a terminal in VS Code and run "mkdir -p /home/vscode/.aspnet/https && mv aspnetapp.pfx /home/vscode/.aspnet/https" + // + // * If only using Remote - Containers with a local container, uncomment this line instead: + // "mounts": [ "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind" ], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "dotnet tool restore && dotnet restore", + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..269c4c4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.fs] +indent_size = 4 +indent_style = space diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..50d7cce --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,26 @@ +name: Build + +on: [push, pull_request] + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest] + dotnet: [5.0.202] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ matrix.dotnet }} + - name: Restore tools + run: dotnet tool restore + - name: Restore dependencies + run: dotnet restore + - name: Run build + run: dotnet build + - name: Run tests + run: dotnet test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..d7db416 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,56 @@ +name: Publish + +on: + push: + tags: + - "v*.*.*" + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest] + dotnet: [5.0.202] + runs-on: ${{ matrix.os }} + + steps: + - name: Get version from tag + id: tag_name + run: | + echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v} + shell: bash + - uses: actions/checkout@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ matrix.dotnet }} + - name: Restore tools + run: dotnet tool restore + - name: Run Test + run: dotnet test + - name: Pack + run: dotnet fsi ./pack.fsx + - name: Get Changelog Entry + id: changelog_reader + uses: mindsers/changelog-reader-action@v2.0.0 + with: + version: ${{ steps.tag_name.outputs.current_version }} + - name: Create Release + id: create_release + uses: actions/create-release@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + body: ${{ steps.changelog_reader.outputs.changes }} + draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} + prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v1-release + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: out/*.nupkg + tag: ${{ github.ref }} + overwrite: true + file_glob: true diff --git a/.gitignore b/.gitignore index 3d9a472..8b91be9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ -.idea/ -.vs/ -.vscode/ -bin -obj -*.DotSettings.user \ No newline at end of file +.idea/ +.vs/ +bin/ +obj/ +out/ +*.DotSettings.user +.ionide/ +.fake/ +packages/ +paket-files/ diff --git a/.paket/Paket.Restore.targets b/.paket/Paket.Restore.targets new file mode 100644 index 0000000..0ec2816 --- /dev/null +++ b/.paket/Paket.Restore.targets @@ -0,0 +1,494 @@ + + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + $(MSBuildVersion) + 15.0.0 + false + true + + true + $(MSBuildThisFileDirectory) + $(MSBuildThisFileDirectory)..\ + $(PaketRootPath)paket-files\paket.restore.cached + $(PaketRootPath)paket.lock + classic + proj + assembly + native + /Library/Frameworks/Mono.framework/Commands/mono + mono + + + $(PaketRootPath)paket.bootstrapper.exe + $(PaketToolsPath)paket.bootstrapper.exe + $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ + + "$(PaketBootStrapperExePath)" + $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" + + + + + true + true + + + True + + + False + + $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) + + + + + + + + + $(PaketRootPath)paket + $(PaketToolsPath)paket + + + + + + $(PaketRootPath)paket.exe + $(PaketToolsPath)paket.exe + + + + + + <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) + <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) + <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false + + + + + + + + + + + <_PaketCommand>dotnet paket + + + + + + $(PaketToolsPath)paket + $(PaketBootStrapperExeDir)paket + + + paket + + + + + <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) + <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" + <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" + <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" + + + + + + + + + + + + + + + + + + + + + true + $(NoWarn);NU1603;NU1604;NU1605;NU1608 + false + true + + + + + + + + + $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) + + + + + + + $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) + $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) + + + + + %(PaketRestoreCachedKeyValue.Value) + %(PaketRestoreCachedKeyValue.Value) + + + + + true + false + true + + + + + true + + + + + + + + + + + + + + + + + + + $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached + + $(MSBuildProjectFullPath).paket.references + + $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references + + $(MSBuildProjectDirectory)\paket.references + + false + true + true + references-file-or-cache-not-found + + + + + $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) + $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) + references-file + false + + + + + false + + + + + true + target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) + + + + + + + + + + + false + true + + + + + + + + + + + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) + + + %(PaketReferencesFileLinesInfo.PackageVersion) + All + runtime + runtime + true + true + + + + + $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools + + + + + + + + + $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) + $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) + + + %(PaketCliToolFileLinesInfo.PackageVersion) + + + + + + + + + + false + + + + + + <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> + + + + + + $(MSBuildProjectDirectory)/$(MSBuildProjectFile) + true + false + true + false + true + false + true + false + true + $(PaketIntermediateOutputPath)\$(Configuration) + $(PaketIntermediateOutputPath) + + + + <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> + + + + + + + + + + + + + + + + + + + + + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..23fd35f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..78c42e9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.6.0] +### Changed +- [from upstream] Fixed no fields union case (de)serialization - not backward compatible. +- [from upstream] Refactored union support. + +## [0.5.0] +### Added +- Support for Set and ResizeArray + +### Changed +- Modernize tests +- Dependency FSharp.Core 4.3.4 (F# 4.1 - https://github.com/dotnet/fsharp/blob/main/docs/fsharp-core-notes.md - https://en.wikipedia.org/wiki/F_Sharp_(programming_language)) + +### Fixed +- Null in untyped deserializing + +## [0.4.0] +- Fixed no fields union case (de)serialization - not backward compatible. + +## [0.3.7] +- Added support for numeric types: byte, sbyte, int16, uint16, uint, uint64, bigint +- Added support for floating point single type + +## [0.3.6] +- Documentation cleanup + +## [0.3.5] +- Moved to Release build + +## [0.3.4] +- Moved to .NET Standard + +## [0.3.3] +- Added .NET Core support + +## [0.3.2] +- Added Transform for Uri type + +## [0.3.1] +- Fixed FSharp.Core dependency to allow newer versions + +## [0.3] +- Fix for tuples containing option types +- Support for char type +- Support for enums based on byte and char types +- Configurable enum mode +- Configurable unformatted setting + +## [0.2] +- Single case union as wrapped type + +## [0.1] +- Initial release diff --git a/FSharp.Json.Giraffe/FSharp.Json.Giraffe.fsproj b/FSharp.Json.Giraffe/FSharp.Json.Giraffe.fsproj deleted file mode 100644 index 2aa8c08..0000000 --- a/FSharp.Json.Giraffe/FSharp.Json.Giraffe.fsproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - netstandard2.0 - Integration of F# JSON into Giraffe - vsapronov - https://github.com/vsapronov/FSharp.Json - Copyright 2020 - F# JSON serialization Giraffe - https://github.com/vsapronov/FSharp.Json - 0.4.1 - - - - - - - - - - - diff --git a/FSharp.Json.Giraffe/Library.fs b/FSharp.Json.Giraffe/Library.fs deleted file mode 100644 index d06bf5b..0000000 --- a/FSharp.Json.Giraffe/Library.fs +++ /dev/null @@ -1,5 +0,0 @@ -namespace FSharp.Json.Giraffe - -module Say = - let hello name = - printfn "Hello %s" name diff --git a/FSharp.Json.Tests/App.config b/FSharp.Json.Tests/App.config deleted file mode 100644 index 756bc0e..0000000 --- a/FSharp.Json.Tests/App.config +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - True - - - - diff --git a/FSharp.Json.Tests/AsJson.fs b/FSharp.Json.Tests/AsJson.fs index 59feb9c..12a46ac 100644 --- a/FSharp.Json.Tests/AsJson.fs +++ b/FSharp.Json.Tests/AsJson.fs @@ -4,14 +4,15 @@ module AsJson = open System open NUnit.Framework - type AsJsonRecord = { - [] - value: string - } + type AsJsonRecord = + { [] + value: string } [] let ``AsJson member serialization - object`` () = - let value = { AsJsonRecord.value = """{"property":"The value"}""" } + let value = + { AsJsonRecord.value = """{"property":"The value"}""" } + let actual = Json.serializeU value let expected = """{"value":{"property":"The value"}}""" Assert.AreEqual(expected, actual) @@ -20,7 +21,10 @@ module AsJson = let ``AsJson member deserialization - object`` () = let json = """{"value":{"property":"The value"}}""" let actual = Json.deserialize json - let expected = { AsJsonRecord.value = """{"property":"The value"}"""} + + let expected = + { AsJsonRecord.value = """{"property":"The value"}""" } + Assert.AreEqual(expected, actual) [] @@ -37,10 +41,9 @@ module AsJson = let actual = Json.deserialize json Assert.AreEqual(expected, actual) - type AsJsonOptionalRecord = { - [] - value: string option - } + type AsJsonOptionalRecord = + { [] + value: string option } [] let ``AsJson member serialization - None`` () = @@ -51,6 +54,8 @@ module AsJson = [] let ``AsJson member deserialization - null`` () = - let actual = Json.deserialize """{"value":null}""" + let actual = + Json.deserialize """{"value":null}""" + let expected = { AsJsonOptionalRecord.value = None } Assert.AreEqual(expected, actual) diff --git a/FSharp.Json.Tests/Collections.fs b/FSharp.Json.Tests/Collections.fs index e36bb85..8b4d526 100644 --- a/FSharp.Json.Tests/Collections.fs +++ b/FSharp.Json.Tests/Collections.fs @@ -7,41 +7,89 @@ module Collections = [] let ``Array serialization to JSON array`` () = let expected = """["some","text"]""" - let value = [|"some"; "text"|] + let value = [| "some"; "text" |] let actual = Json.serializeU value Assert.AreEqual(expected, actual) [] let ``List serialization to JSON array`` () = let expected = """["some","text"]""" - let value = ["some"; "text"] + let value = [ "some"; "text" ] + let actual = Json.serializeU value + Assert.AreEqual(expected, actual) + + [] + let ``Set serialization to JSON array`` () = + let expected = """["some","text"]""" + let value = [ "some"; "text" ] |> Set.ofList + let actual = Json.serializeU value + Assert.AreEqual(expected, actual) + + [] + let ``ResizeArray serialization to JSON array`` () = + let expected = """["some","text"]""" + let value = [ "some"; "text" ] |> ResizeArray let actual = Json.serializeU value Assert.AreEqual(expected, actual) [] let ``Array serialization/deserialization`` () = - let expected = [|"some"; "text"|] - let json = Json.serialize(expected) + let expected = [| "some"; "text" |] + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) [] let ``List serialization/deserialization`` () = - let expected = ["some"; "text"] - let json = Json.serialize(expected) + let expected = [ "some"; "text" ] + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) + [] + let ``Set serialization/deserialization`` () = + let expected = [ "some"; "text" ] + let json = Json.serialize (expected) + let actual = Json.deserialize> json + Assert.AreEqual(expected, actual) + + [] + let ``ResizeArray serialization/deserialization`` () = + let expected = [ "some"; "text" ] |> ResizeArray + let json = Json.serialize (expected) + + let actual = + Json.deserialize> json + + Assert.AreEqual(expected, actual) + [] let ``Array empty serialization/deserialization`` () = let expected = [||] - let json = Json.serialize(expected) + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) [] let ``List empty serialization/deserialization`` () = - let expected = [] - let json = Json.serialize(expected) + let expected = List.empty + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) + + [] + let ``Set empty serialization/deserialization`` () = + let expected = Set.empty + let json = Json.serialize (expected) + let actual = Json.deserialize> json + Assert.AreEqual(expected, actual) + + [] + let ``ResizeArray empty serialization/deserialization`` () = + let expected = ResizeArray() + let json = Json.serialize (expected) + + let actual = + Json.deserialize> json + + Assert.AreEqual(expected, actual) diff --git a/FSharp.Json.Tests/DateTimeFormat.fs b/FSharp.Json.Tests/DateTimeFormat.fs index 6f73d50..de3542f 100644 --- a/FSharp.Json.Tests/DateTimeFormat.fs +++ b/FSharp.Json.Tests/DateTimeFormat.fs @@ -4,34 +4,41 @@ module DateTimeFormat = open System open NUnit.Framework - type TheDateTimeOffset = { - value: DateTimeOffset - } + type TheDateTimeOffset = { value: DateTimeOffset } [] let ``DateTimeOffset member serialization Newtonsoft format`` () = - let expected = """{"value":"2017-11-04T22:50:45.1230000-04:00"}""" - let value = new DateTimeOffset(2017, 11, 4, 22, 50, 45, 123, new TimeSpan(-4, 0, 0)) + let expected = + """{"value":"2017-11-04T22:50:45.1230000-04:00"}""" + + let value = + new DateTimeOffset(2017, 11, 4, 22, 50, 45, 123, new TimeSpan(-4, 0, 0)) + let theRecord = { TheDateTimeOffset.value = value } - let actual = Json.serializeU(theRecord) + let actual = Json.serializeU (theRecord) Assert.AreEqual(expected, actual) [] let ``DateTimeOffset member deserialization without offset`` () = let json = """{"value":"2017-11-04T22:50:45"}""" - let value = new DateTimeOffset(2017, 11, 4, 22, 50, 45, new TimeSpan(0L)) + + let value = + new DateTimeOffset(2017, 11, 4, 22, 50, 45, new TimeSpan(0L)) + let expected = { TheDateTimeOffset.value = value } let actual = Json.deserialize json Assert.AreEqual(expected, actual) - type DateTimeFormat = { - [] - value: DateTime - } + type DateTimeFormat = + { [] + value: DateTime } [] let ``DateTime member serialization custom format`` () = let expected = """{"value":"2017-11-04T22:50:45"}""" - let value = Json.deserialize expected - let actual = Json.serializeU(value) - Assert.AreEqual(expected, actual) \ No newline at end of file + + let value = + Json.deserialize expected + + let actual = Json.serializeU (value) + Assert.AreEqual(expected, actual) diff --git a/FSharp.Json.Tests/Default.fs b/FSharp.Json.Tests/Default.fs index 3ad8f1f..b1e7541 100644 --- a/FSharp.Json.Tests/Default.fs +++ b/FSharp.Json.Tests/Default.fs @@ -4,14 +4,15 @@ module Default = open System open NUnit.Framework - type AnnotatedRecord = { - [] - Value: string - } + type AnnotatedRecord = + { [] + Value: string } [] let ``Default value for omitted field`` () = - let expected = { AnnotatedRecord.Value = "The default value" } + let expected = + { AnnotatedRecord.Value = "The default value" } + let json = "{}" let actual = Json.deserialize json Assert.AreEqual(expected, actual) diff --git a/FSharp.Json.Tests/Enum.fs b/FSharp.Json.Tests/Enum.fs index 6d772ab..ac927b1 100644 --- a/FSharp.Json.Tests/Enum.fs +++ b/FSharp.Json.Tests/Enum.fs @@ -2,20 +2,20 @@ module Enum = open NUnit.Framework - + type NumberEnum = - | One = 1 - | Two = 2 - | Three = 3 + | One = 1 + | Two = 2 + | Three = 3 - type TheNumberEnum = { - value: NumberEnum - } + type TheNumberEnum = { value: NumberEnum } [] let ``Enum serialization/deserialization`` () = - let expected = { TheNumberEnum.value = NumberEnum.Three } - let json = Json.serialize(expected) + let expected = + { TheNumberEnum.value = NumberEnum.Three } + + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) @@ -36,50 +36,65 @@ module Enum = [] let ``Enum serialization - config setting`` () = let value = { TheNumberEnum.value = NumberEnum.Two } - let config = JsonConfig.create(unformatted = true, enumValue = EnumMode.Value) + + let config = + JsonConfig.create (unformatted = true, enumValue = EnumMode.Value) + let actual = Json.serializeEx config value let expected = """{"value":2}""" Assert.AreEqual(expected, actual) type LetterEnum = - | LetterA = 'a' - | LetterB = 'b' - | LetterC = 'c' + | LetterA = 'a' + | LetterB = 'b' + | LetterC = 'c' - type TheAttributedLetterEnum = { - [] - value: LetterEnum - } + type TheAttributedLetterEnum = + { [] + value: LetterEnum } [] let ``Letter Enum value deserialization`` () = - let expected = { TheAttributedLetterEnum.value = LetterEnum.LetterB } + let expected = + { TheAttributedLetterEnum.value = LetterEnum.LetterB } + let json = """{"value":"b"}""" - let actual = Json.deserialize json + + let actual = + Json.deserialize json + Assert.AreEqual(expected, actual) [] let ``Letter Enum value serialization`` () = - let value = { TheAttributedLetterEnum.value = LetterEnum.LetterC } + let value = + { TheAttributedLetterEnum.value = LetterEnum.LetterC } + let actual = Json.serializeU value let expected = """{"value":"c"}""" Assert.AreEqual(expected, actual) - type TheAttributedNumberEnum = { - [] - value: NumberEnum - } + type TheAttributedNumberEnum = + { [] + value: NumberEnum } [] let ``Number Enum value deserialization`` () = - let expected = { TheAttributedNumberEnum.value = NumberEnum.Two } + let expected = + { TheAttributedNumberEnum.value = NumberEnum.Two } + let json = """{"value":2}""" - let actual = Json.deserialize json + + let actual = + Json.deserialize json + Assert.AreEqual(expected, actual) [] let ``Number Enum value serialization`` () = - let value = { TheAttributedNumberEnum.value = NumberEnum.Three } + let value = + { TheAttributedNumberEnum.value = NumberEnum.Three } + let actual = Json.serializeU value let expected = """{"value":3}""" Assert.AreEqual(expected, actual) diff --git a/FSharp.Json.Tests/FSharp.Json.Tests.fsproj b/FSharp.Json.Tests/FSharp.Json.Tests.fsproj index 3a636bc..bf3a5f2 100644 --- a/FSharp.Json.Tests/FSharp.Json.Tests.fsproj +++ b/FSharp.Json.Tests/FSharp.Json.Tests.fsproj @@ -1,33 +1,25 @@ - - - - netcoreapp2.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + net5.0 + + + + + + + + + + + + + + + + + + + + + diff --git a/FSharp.Json.Tests/Map.fs b/FSharp.Json.Tests/Map.fs index e041118..c8ed03f 100644 --- a/FSharp.Json.Tests/Map.fs +++ b/FSharp.Json.Tests/Map.fs @@ -7,28 +7,42 @@ module Map = [] let ``Map serialization`` () = let expected = """{"key":"value"}""" - let value = Map.ofList [("key", "value")] + let value = Map.ofList [ ("key", "value") ] let actual = Json.serializeU value Assert.AreEqual(expected, actual) [] let ``Map deserialization`` () = let json = """{"key":"value"}""" - let expected = Map.ofList [("key", "value")] - let actual = Json.deserialize> json + let expected = Map.ofList [ ("key", "value") ] + + let actual = + Json.deserialize> json + Assert.AreEqual(expected, actual) [] let ``Map serialization`` () = let expected = """{"key1":"value","key2":123}""" - let value = Map.ofList [("key1", "value" :> obj); ("key2", 123 :> obj)] + + let value = + Map.ofList [ ("key1", "value" :> obj) + ("key2", 123 :> obj) ] + let actual = Json.serializeU value Assert.AreEqual(expected, actual) [] let ``Map deserialization`` () = let json = """{"key1":"value","key2":123}""" - let expected = Map.ofList [("key1", "value" :> obj); ("key2", 123 :> obj)] - let config = JsonConfig.create(allowUntyped = true) - let actual = Json.deserializeEx> config json + + let expected = + Map.ofList [ ("key1", "value" :> obj) + ("key2", 123 :> obj) ] + + let config = JsonConfig.create (allowUntyped = true) + + let actual = + Json.deserializeEx> config json + Assert.AreEqual(expected, actual) diff --git a/FSharp.Json.Tests/Namings.fs b/FSharp.Json.Tests/Namings.fs index 72f0a64..49edf02 100644 --- a/FSharp.Json.Tests/Namings.fs +++ b/FSharp.Json.Tests/Namings.fs @@ -6,19 +6,19 @@ module LowerCamelCase = [] let ``Single word`` () = - Assert.AreEqual("property", Json.lowerCamelCase("Property")) + Assert.AreEqual("property", Json.lowerCamelCase ("Property")) [] let ``No changes needed`` () = - Assert.AreEqual("property", Json.lowerCamelCase("property")) + Assert.AreEqual("property", Json.lowerCamelCase ("property")) [] let ``Two words`` () = - Assert.AreEqual("twoWords", Json.lowerCamelCase("TwoWords")) + Assert.AreEqual("twoWords", Json.lowerCamelCase ("TwoWords")) [] let ``Abbreviations`` () = - Assert.AreEqual("somethingTbd", Json.lowerCamelCase("SomethingTBD")) + Assert.AreEqual("somethingTbd", Json.lowerCamelCase ("SomethingTBD")) module SnakeCase = open System @@ -26,16 +26,16 @@ module SnakeCase = [] let ``Single word`` () = - Assert.AreEqual("property", Json.snakeCase("Property")) + Assert.AreEqual("property", Json.snakeCase ("Property")) [] let ``No changes needed`` () = - Assert.AreEqual("property", Json.snakeCase("property")) + Assert.AreEqual("property", Json.snakeCase ("property")) [] let ``Two words`` () = - Assert.AreEqual("two_words", Json.snakeCase("TwoWords")) + Assert.AreEqual("two_words", Json.snakeCase ("TwoWords")) [] let ``Abbreviations`` () = - Assert.AreEqual("something_tbd", Json.snakeCase("SomethingTBD")) + Assert.AreEqual("something_tbd", Json.snakeCase ("SomethingTBD")) diff --git a/FSharp.Json.Tests/Object.fs b/FSharp.Json.Tests/Object.fs index a5ee4e4..349b1c3 100644 --- a/FSharp.Json.Tests/Object.fs +++ b/FSharp.Json.Tests/Object.fs @@ -4,14 +4,38 @@ module Object = open System open NUnit.Framework - type ObjectRecord = { - value: obj - } + type ObjectRecord = { value: obj } [] let ``Object serialization/deserialization`` () = let expected = { ObjectRecord.value = "The string" } - let config = JsonConfig.create(allowUntyped = true, unformatted = true) + + let config = + JsonConfig.create (allowUntyped = true, unformatted = true) + let json = Json.serializeEx config expected - let actual = Json.deserializeEx config json + + let actual = + Json.deserializeEx config json + + Assert.AreEqual(expected, actual) + + [] + let ``Object with null serialization/deserialization`` () = + let expectedMap = + Map.empty + |> Map.add "stringValue" ("The string" :> obj) + |> Map.add "intValue" (42M :> obj) + |> Map.add "nullValue" null + + let expected = { ObjectRecord.value = expectedMap } + + let config = + JsonConfig.create (allowUntyped = true, unformatted = true) + + let json = Json.serializeEx config expected + + let actual = + Json.deserializeEx config json + Assert.AreEqual(expected, actual) diff --git a/FSharp.Json.Tests/Option.fs b/FSharp.Json.Tests/Option.fs index 366c2e5..da52bbf 100644 --- a/FSharp.Json.Tests/Option.fs +++ b/FSharp.Json.Tests/Option.fs @@ -1,11 +1,9 @@ namespace FSharp.Json -module Option = +module Option = open NUnit.Framework - type TheOption = { - value: string option - } + type TheOption = { value: string option } [] let ``Option None serialization/deserialization`` () = @@ -30,36 +28,42 @@ module Option = [] let ``Option None serialization into null by default`` () = - let actual = Json.serializeU { TheOption.value = None } + let actual = + Json.serializeU { TheOption.value = None } + let expected = """{"value":null}""" Assert.AreEqual(expected, actual) [] let ``Option Some serialization/deserialization`` () = let expected = { TheOption.value = Some "The string" } - let json = Json.serialize(expected) + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) - - type TheNonOption = { - value: string - } + + type TheNonOption = { value: string } [] let ``The null value is not allowed for non option type`` () = let json = """{"value":null}""" - let ex = Assert.Throws(fun () -> Json.deserialize json |> ignore) + + let ex = + Assert.Throws(fun () -> Json.deserialize json |> ignore) + Assert.IsNotNull(ex) let expectedPath = "value" - Assert.AreEqual(expectedPath, ex.Path.toString()) + Assert.AreEqual(expectedPath, ex.Path.toString ()) [] let ``Omitted value is not allowed for non option type`` () = let json = """{}""" - let ex = Assert.Throws(fun () -> Json.deserialize json |> ignore) + + let ex = + Assert.Throws(fun () -> Json.deserialize json |> ignore) + Assert.IsNotNull(ex) let expectedPath = "value" - Assert.AreEqual(expectedPath, ex.Path.toString()) + Assert.AreEqual(expectedPath, ex.Path.toString ()) let assertDeserializationThrows<'T> (config: JsonConfig) (json: string) = Assert.Throws(fun () -> Json.deserializeEx<'T> config json |> ignore) @@ -67,37 +71,60 @@ module Option = [] let ``JsonConfig.deserializeOption - member with None value can't be omitted`` () = let json = """{}""" - let config = JsonConfig.create(deserializeOption = DeserializeOption.RequireNull) - let ex = assertDeserializationThrows config json + + let config = + JsonConfig.create (deserializeOption = DeserializeOption.RequireNull) + + let ex = + assertDeserializationThrows config json + let expectedPath = "value" - Assert.AreEqual(expectedPath, ex.Path.toString()) + Assert.AreEqual(expectedPath, ex.Path.toString ()) [] let ``JsonConfig.deserializeOption - member with None value is ommitted`` () = let json = """{}""" let expected = { TheOption.value = None } - let config = JsonConfig.create(deserializeOption = DeserializeOption.AllowOmit) - let actual = Json.deserializeEx config json + + let config = + JsonConfig.create (deserializeOption = DeserializeOption.AllowOmit) + + let actual = + Json.deserializeEx config json + Assert.AreEqual(expected, actual) [] let ``JsonConfig.deserializeOption - member is null`` () = let json = """{"value":null}""" let expected = { TheOption.value = None } - let config = JsonConfig.create(deserializeOption = DeserializeOption.RequireNull) - let actual = Json.deserializeEx config json + + let config = + JsonConfig.create (deserializeOption = DeserializeOption.RequireNull) + + let actual = + Json.deserializeEx config json + Assert.AreEqual(expected, actual) [] let ``JsonConfig.serializeNone - member with None value as null`` () = - let config = JsonConfig.create(unformatted = true, serializeNone = SerializeNone.Null) - let actual = Json.serializeEx config { TheOption.value = None } + let config = + JsonConfig.create (unformatted = true, serializeNone = SerializeNone.Null) + + let actual = + Json.serializeEx config { TheOption.value = None } + let expected = """{"value":null}""" Assert.AreEqual(expected, actual) [] let ``JsonConfig.serializeNone - member with None value is ommitted`` () = - let config = JsonConfig.create(unformatted = true, serializeNone = SerializeNone.Omit) - let actual = Json.serializeEx config { TheOption.value = None } + let config = + JsonConfig.create (unformatted = true, serializeNone = SerializeNone.Omit) + + let actual = + Json.serializeEx config { TheOption.value = None } + let expected = """{}""" - Assert.AreEqual(expected, actual) \ No newline at end of file + Assert.AreEqual(expected, actual) diff --git a/FSharp.Json.Tests/Record.fs b/FSharp.Json.Tests/Record.fs index f825bd6..51bea37 100644 --- a/FSharp.Json.Tests/Record.fs +++ b/FSharp.Json.Tests/Record.fs @@ -4,72 +4,68 @@ module Record = open System open NUnit.Framework - type TheRecord = { - theString: string - theDecimal: decimal - theBool: bool - theByte: byte - theSByte: sbyte - theInt16: int16 - theUInt16: uint16 - theInt: int - theUInt: uint32 - theInt64: int64 - theUInt64: uint64 - theBigint: bigint - theSingle: single - theFloat: float - theGuid: Guid - theDateTime: DateTime - theDateTimeOffset: DateTimeOffset - theChar: char - } + type TheRecord = + { theString: string + theDecimal: decimal + theBool: bool + theByte: byte + theSByte: sbyte + theInt16: int16 + theUInt16: uint16 + theInt: int + theUInt: uint32 + theInt64: int64 + theUInt64: uint64 + theBigint: bigint + theSingle: single + theFloat: float + theGuid: Guid + theDateTime: DateTime + theDateTimeOffset: DateTimeOffset + theChar: char } [] let ``Plain fields serialization/deserialization`` () = - let expected = { - theString = "The string" - theDecimal = 123M - theBool = true - theByte = 123uy - theSByte = 123y - theInt16 = 123s - theUInt16 = 123us - theInt = 123 - theUInt = 123u - theInt64 = 123L - theUInt64 = 123UL - theBigint = 123I - theSingle = 123.0f - theFloat = 123.123 - theGuid = Guid.NewGuid() - theDateTime = DateTime.Now - theDateTimeOffset = DateTimeOffset.Now - theChar = 'a' - } - let json = Json.serialize(expected) + let expected = + { theString = "The string" + theDecimal = 123M + theBool = true + theByte = 123uy + theSByte = 123y + theInt16 = 123s + theUInt16 = 123us + theInt = 123 + theUInt = 123u + theInt64 = 123L + theUInt64 = 123UL + theBigint = 123I + theSingle = 123.0f + theFloat = 123.123 + theGuid = Guid.NewGuid() + theDateTime = DateTime.Now + theDateTimeOffset = DateTimeOffset.Now + theChar = 'a' } + + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) - type InnerRecord = { - value: string - } + type InnerRecord = { value: string } - type OuterRecord = { - inner: InnerRecord - } + type OuterRecord = { inner: InnerRecord } [] let ``Record field serialization/deserialization`` () = - let expected = { OuterRecord.inner = { InnerRecord.value = "The string" } } - let json = Json.serialize(expected) + let expected = + { OuterRecord.inner = { InnerRecord.value = "The string" } } + + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) - type AnnotatedRecord = { - [] - Value: string - } + type AnnotatedRecord = + { [] + Value: string } [] let ``Record custom field name serialization`` () = @@ -85,22 +81,32 @@ module Record = let actual = Json.deserialize json Assert.AreEqual(expected, actual) - type UpperCaseRecord = { - SomeValue: string - } + type UpperCaseRecord = { SomeValue: string } [] let ``Record field name serialization with snake case naming`` () = let expected = """{"some_value":"The string"}""" - let value = { UpperCaseRecord.SomeValue = "The string" } - let config = JsonConfig.create(unformatted = true, jsonFieldNaming = Json.snakeCase) + + let value = + { UpperCaseRecord.SomeValue = "The string" } + + let config = + JsonConfig.create (unformatted = true, jsonFieldNaming = Json.snakeCase) + let actual = Json.serializeEx config value Assert.AreEqual(expected, actual) [] let ``Record field name deserialization with snake case naming`` () = let json = """{"some_value":"The string"}""" - let expected = { UpperCaseRecord.SomeValue = "The string" } - let config = JsonConfig.create(jsonFieldNaming = Json.snakeCase) - let actual = Json.deserializeEx config json + + let expected = + { UpperCaseRecord.SomeValue = "The string" } + + let config = + JsonConfig.create (jsonFieldNaming = Json.snakeCase) + + let actual = + Json.deserializeEx config json + Assert.AreEqual(expected, actual) diff --git a/FSharp.Json.Tests/Transforms.fs b/FSharp.Json.Tests/Transforms.fs index 92c6b22..c1ee668 100644 --- a/FSharp.Json.Tests/Transforms.fs +++ b/FSharp.Json.Tests/Transforms.fs @@ -4,56 +4,68 @@ module Transforms = open System open NUnit.Framework - type DateTimeRecord = { - [)>] - value: DateTime - } + type DateTimeRecord = + { [)>] + value: DateTime } [] let ``DateTime as Epoch serialization`` () = - let actual = Json.serializeU { DateTimeRecord.value = new DateTime(2017, 11, 5, 22, 50, 45) } + let actual = + Json.serializeU { DateTimeRecord.value = new DateTime(2017, 11, 5, 22, 50, 45) } + let expected = """{"value":1509922245}""" Assert.AreEqual(expected, actual) [] let ``DateTime as Epoch deserialization`` () = - let expected = { DateTimeRecord.value = new DateTime(2017, 11, 5, 22, 50, 45) } + let expected = + { DateTimeRecord.value = new DateTime(2017, 11, 5, 22, 50, 45) } + let json = """{"value":1509922245}""" let actual = Json.deserialize json Assert.AreEqual(expected, actual) - type DateTimeOffsetRecord = { - [)>] - value: DateTimeOffset - } + type DateTimeOffsetRecord = + { [)>] + value: DateTimeOffset } [] let ``DateTimeOffset as Epoch serialization`` () = - let actual = Json.serializeU { DateTimeOffsetRecord.value = new DateTimeOffset(2017, 11, 5, 22, 50, 45, TimeSpan(0L)) } + let actual = + Json.serializeU { DateTimeOffsetRecord.value = new DateTimeOffset(2017, 11, 5, 22, 50, 45, TimeSpan(0L)) } + let expected = """{"value":1509922245}""" Assert.AreEqual(expected, actual) [] let ``DateTimeOffset as Epoch deserialization`` () = - let expected = { DateTimeOffsetRecord.value = new DateTimeOffset(2017, 11, 5, 22, 50, 45, TimeSpan(0L)) } + let expected = + { DateTimeOffsetRecord.value = new DateTimeOffset(2017, 11, 5, 22, 50, 45, TimeSpan(0L)) } + let json = """{"value":1509922245}""" - let actual = Json.deserialize json + + let actual = + Json.deserialize json + Assert.AreEqual(expected, actual) - type UriRecord = { - [)>] - value : System.Uri - } + type UriRecord = + { [)>] + value: System.Uri } [] let ``System.Uri as string serialization`` () = - let actual = Json.serializeU { UriRecord.value = Uri("http://localhost:8080/") } + let actual = + Json.serializeU { UriRecord.value = Uri("http://localhost:8080/") } + let expected = """{"value":"http://localhost:8080/"}""" Assert.AreEqual(expected, actual) [] let ``System.Uri as string deserialization`` () = - let expected = { UriRecord.value = new Uri("http://localhost:8080/") } + let expected = + { UriRecord.value = new Uri("http://localhost:8080/") } + let json = """{"value":"http://localhost:8080/"}""" let actual = Json.deserialize json Assert.AreEqual(expected, actual) @@ -61,4 +73,6 @@ module Transforms = [] let ``Corrupted uri throws exception`` () = let json = """{"value":"notreallyauri"}""" - Assert.Throws(fun () -> Json.deserialize json |> ignore) |> ignore \ No newline at end of file + + Assert.Throws(fun () -> Json.deserialize json |> ignore) + |> ignore diff --git a/FSharp.Json.Tests/Tuple.fs b/FSharp.Json.Tests/Tuple.fs index 99bcd88..7654cb0 100644 --- a/FSharp.Json.Tests/Tuple.fs +++ b/FSharp.Json.Tests/Tuple.fs @@ -3,38 +3,45 @@ module Tuple = open NUnit.Framework - type TheTuple = { - value: string*int*bool - } + type TheTuple = { value: string * int * bool } [] let ``Tuple serialization/deserialization`` () = - let expected = { TheTuple.value = ("The string", 123, true) } - let json = Json.serialize(expected) + let expected = + { TheTuple.value = ("The string", 123, true) } + + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) [] let ``Tuple serialized as array`` () = - let value = { TheTuple.value = ("The string", 123, true) } + let value = + { TheTuple.value = ("The string", 123, true) } + let actual = Json.serializeU value let expected = """{"value":["The string",123,true]}""" Assert.AreEqual(expected, actual) - type TheTupleWithOption = { - value: string*int option*bool - } + type TheTupleWithOption = { value: string * int option * bool } [] let ``Tuple with optional serialization`` () = - let value = { TheTupleWithOption.value = ("The string", None, true) } + let value = + { TheTupleWithOption.value = ("The string", None, true) } + let actual = Json.serializeU value let expected = """{"value":["The string",null,true]}""" Assert.AreEqual(expected, actual) [] let ``Tuple with optional deserialization`` () = - let expected = { TheTupleWithOption.value = ("The string", None, true) } + let expected = + { TheTupleWithOption.value = ("The string", None, true) } + let json = """{"value":["The string",null,true]}""" - let actual = Json.deserialize json + + let actual = + Json.deserialize json + Assert.AreEqual(expected, actual) diff --git a/FSharp.Json.Tests/Union.fs b/FSharp.Json.Tests/Union.fs index 0de36fb..e67e836 100644 --- a/FSharp.Json.Tests/Union.fs +++ b/FSharp.Json.Tests/Union.fs @@ -3,20 +3,16 @@ module Union = open NUnit.Framework - type TheRecord = { - Value: string - } + type TheRecord = { Value: string } type TheUnion = - | NoFieldCase - | OneFieldCase of string - | ManyFieldsCase of string*int - | RecordCase of TheRecord - - type OtherRecord = { - Union: TheUnion - } - + | NoFieldCase + | OneFieldCase of string + | ManyFieldsCase of string * int + | RecordCase of TheRecord + + type OtherRecord = { Union: TheUnion } + [] let ``No field case serialization`` () = let value = NoFieldCase @@ -34,10 +30,10 @@ module Union = [] let ``Union no field case deserialization`` () = let expected = { OtherRecord.Union = NoFieldCase } - let json = Json.serialize(expected) + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) - + [] let ``Union one field case serialization`` () = let value = OneFieldCase "The string" @@ -48,27 +44,29 @@ module Union = [] let ``Union one field case deserialization`` () = let expected = OneFieldCase "The string" - let json = Json.serialize(expected) + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) [] let ``Union many fields case serialization`` () = - let expected = ManyFieldsCase ("The string", 123) - let json = Json.serialize(expected) + let expected = ManyFieldsCase("The string", 123) + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) [] let ``Union record field case serialization`` () = - let expected = RecordCase {TheRecord.Value = "The string"} - let json = Json.serialize(expected) + let expected = + RecordCase { TheRecord.Value = "The string" } + + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) type TheCasesUnion = - | [] StringCase of string - | [] IntCase of int + | [] StringCase of string + | [] IntCase of int [] let ``Union custom case name serialization`` () = @@ -84,45 +82,64 @@ module Union = let actual = Json.deserialize json Assert.AreEqual(expected, actual) - [] + [] type TheAnnotatedUnion = - | StringCase of string - | IntCase of int + | StringCase of string + | IntCase of int [] let ``Union key-value serialization`` () = - let value = TheAnnotatedUnion.StringCase "The string" + let value = + TheAnnotatedUnion.StringCase "The string" + let actual = Json.serializeU value - let expected = """{"casekey":"StringCase","casevalue":"The string"}""" + + let expected = + """{"casekey":"StringCase","casevalue":"The string"}""" + Assert.AreEqual(expected, actual) [] let ``Union key-value deserialization`` () = - let expected = TheAnnotatedUnion.StringCase "The string" - let json = """{"casekey":"StringCase","casevalue":"The string"}""" + let expected = + TheAnnotatedUnion.StringCase "The string" + + let json = + """{"casekey":"StringCase","casevalue":"The string"}""" + let actual = Json.deserialize json Assert.AreEqual(expected, actual) [] let ``Union key-value deserialization (more than 2 fields)`` () = - let expected = TheAnnotatedUnion.StringCase "The string" - let json = """{"casekey":"StringCase","casevalue":"The string","unrelated_property":"unrelated_value"}""" + let expected = + TheAnnotatedUnion.StringCase "The string" + + let json = + """{"casekey":"StringCase","casevalue":"The string","unrelated_property":"unrelated_value"}""" + let actual = Json.deserialize json Assert.AreEqual(expected, actual) [] let ``Union cases serialization with snake case naming`` () = let value = OneFieldCase "The string" - let config = JsonConfig.create(unformatted = true, jsonFieldNaming = Json.snakeCase) + + let config = + JsonConfig.create (unformatted = true, jsonFieldNaming = Json.snakeCase) + let actual = Json.serializeEx config value let expected = """{"one_field_case":"The string"}""" Assert.AreEqual(expected, actual) - + [] let ``Union cases deserialization with snake case naming`` () = let json = """{"one_field_case":"The string"}""" let expected = OneFieldCase "The string" - let config = JsonConfig.create(jsonFieldNaming = Json.snakeCase) + + let config = + JsonConfig.create (jsonFieldNaming = Json.snakeCase) + let actual = Json.deserializeEx config json Assert.AreEqual(expected, actual) @@ -147,31 +164,33 @@ module Union = type SingleCaseUnion = SingleCase of string - type SingleCaseRecord = { - value: SingleCaseUnion - } + type SingleCaseRecord = { value: SingleCaseUnion } [] let ``Union single case serialization`` () = - let value = { SingleCaseRecord.value = SingleCase "The string" } + let value = + { SingleCaseRecord.value = SingleCase "The string" } + let actual = Json.serializeU value let expected = """{"value":"The string"}""" Assert.AreEqual(expected, actual) [] let ``Union single case deserialization`` () = - let expected = { SingleCaseRecord.value = SingleCase "The string" } - let json = Json.serialize(expected) + let expected = + { SingleCaseRecord.value = SingleCase "The string" } + + let json = Json.serialize (expected) let actual = Json.deserialize json Assert.AreEqual(expected, actual) type UnionWithOption = - | Main of string option - | Other + | Main of string option + | Other [] let ``Union case with option serialization`` () = - let value = Main (Some "The string") + let value = Main(Some "The string") let actual = Json.serializeU value let expected = """{"Main":"The string"}""" Assert.AreEqual(expected, actual) @@ -179,7 +198,7 @@ module Union = [] let ``Union case with option deserialization`` () = let json = """{"Main":"The string"}""" - let expected = Main (Some "The string") + let expected = Main(Some "The string") let actual = Json.deserialize json Assert.AreEqual(expected, actual) @@ -189,7 +208,7 @@ module Union = let actual = Json.serializeU value let expected = """{"Main":null}""" Assert.AreEqual(expected, actual) - + [] let ``Union case with option None deserialization`` () = let json = """{"Main":null}""" diff --git a/FSharp.Json.Tests/paket.references b/FSharp.Json.Tests/paket.references new file mode 100644 index 0000000..4807da0 --- /dev/null +++ b/FSharp.Json.Tests/paket.references @@ -0,0 +1,4 @@ +FSharp.Core +Microsoft.NET.Test.Sdk +Nunit +NUnit3TestAdapter diff --git a/FSharp.Json.sln b/FSharp.Json.sln index cb05682..0161a96 100644 --- a/FSharp.Json.sln +++ b/FSharp.Json.sln @@ -1,45 +1,53 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27428.2015 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Json", "FSharp.Json\FSharp.Json.fsproj", "{8C80EE87-ECC6-4BD1-80F4-849FB4FF5B45}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Json.Tests", "FSharp.Json.Tests\FSharp.Json.Tests.fsproj", "{5D95196B-06AF-4158-9FF0-FAF132900A21}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "project", "project", "{60940FD7-1705-49D4-976E-64AC5E15D731}" -ProjectSection(SolutionItems) = preProject - README.md = README.md - RELEASE_NOTES.md = RELEASE_NOTES.md -EndProjectSection -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Json.Giraffe", "FSharp.Json.Giraffe\FSharp.Json.Giraffe.fsproj", "{B650B05D-56FC-48A8-B3F4-90973ABD4138}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".circleci", ".circleci", "{B3C2447E-A02D-419C-8A91-A50AB902C4B5}" -ProjectSection(SolutionItems) = preProject - .circleci\config.yml = .circleci\config.yml -EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {8C80EE87-ECC6-4BD1-80F4-849FB4FF5B45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8C80EE87-ECC6-4BD1-80F4-849FB4FF5B45}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8C80EE87-ECC6-4BD1-80F4-849FB4FF5B45}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8C80EE87-ECC6-4BD1-80F4-849FB4FF5B45}.Release|Any CPU.Build.0 = Release|Any CPU - {5D95196B-06AF-4158-9FF0-FAF132900A21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5D95196B-06AF-4158-9FF0-FAF132900A21}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5D95196B-06AF-4158-9FF0-FAF132900A21}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5D95196B-06AF-4158-9FF0-FAF132900A21}.Release|Any CPU.Build.0 = Release|Any CPU - {B650B05D-56FC-48A8-B3F4-90973ABD4138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B650B05D-56FC-48A8-B3F4-90973ABD4138}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B650B05D-56FC-48A8-B3F4-90973ABD4138}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B650B05D-56FC-48A8-B3F4-90973ABD4138}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {B3C2447E-A02D-419C-8A91-A50AB902C4B5} = {60940FD7-1705-49D4-976E-64AC5E15D731} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.6.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{FE778079-2220-4596-B8D3-570177CE8DFE}" + ProjectSection(SolutionItems) = preProject + paket.dependencies = paket.dependencies + EndProjectSection +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Json", "FSharp.Json\FSharp.Json.fsproj", "{4A63E373-E638-4F47-BD81-2A61C60969ED}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Json.Tests", "FSharp.Json.Tests\FSharp.Json.Tests.fsproj", "{0BE6C74B-2851-49BE-B876-470C88DA4AE7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Debug|x64.Build.0 = Debug|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Debug|x86.Build.0 = Debug|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Release|Any CPU.Build.0 = Release|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Release|x64.ActiveCfg = Release|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Release|x64.Build.0 = Release|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Release|x86.ActiveCfg = Release|Any CPU + {4A63E373-E638-4F47-BD81-2A61C60969ED}.Release|x86.Build.0 = Release|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Debug|x64.Build.0 = Debug|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Debug|x86.ActiveCfg = Debug|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Debug|x86.Build.0 = Debug|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Release|Any CPU.Build.0 = Release|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Release|x64.ActiveCfg = Release|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Release|x64.Build.0 = Release|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Release|x86.ActiveCfg = Release|Any CPU + {0BE6C74B-2851-49BE-B876-470C88DA4AE7}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/FSharp.Json/Core.fs b/FSharp.Json/Core.fs index 5ff7887..ed3e3b7 100644 --- a/FSharp.Json/Core.fs +++ b/FSharp.Json/Core.fs @@ -9,34 +9,45 @@ module internal Core = open Reflection - let findAttributeMember<'T> (memberInfo: MemberInfo): 'T option = - let attributes = memberInfo.GetCustomAttributes(typeof<'T>, false) + let findAttributeMember<'T> (memberInfo: MemberInfo) : 'T option = + let attributes = + memberInfo.GetCustomAttributes(typeof<'T>, false) + match attributes.Length with | 1 -> (attributes.[0]) :?> 'T |> Some | _ -> None - let findAttributeCase<'T> (caseInfo: UnionCaseInfo): 'T option = + let findAttributeCase<'T> (caseInfo: UnionCaseInfo) : 'T option = let attributes = caseInfo.GetCustomAttributes typeof<'T> + match attributes.Length with | 1 -> (attributes.[0]) :?> 'T |> Some | _ -> None - let createTransform (transformType: Type): ITypeTransform = + let createTransform (transformType: Type) : ITypeTransform = let theConstructor = transformType.GetConstructors().[0] theConstructor.Invoke([||]) :?> ITypeTransform let getJsonFieldProperty: PropertyInfo -> JsonField = - findAttributeMember >> Option.defaultValue JsonField.Default |> cacheResult + findAttributeMember + >> Option.defaultValue JsonField.Default + |> cacheResult let getJsonUnion: Type -> JsonUnion = - findAttributeMember >> Option.defaultValue JsonUnion.Default |> cacheResult + findAttributeMember + >> Option.defaultValue JsonUnion.Default + |> cacheResult let getJsonFieldUnionCase: UnionCaseInfo -> JsonField = - findAttributeCase >> Option.defaultValue JsonField.Default |> cacheResult + findAttributeCase + >> Option.defaultValue JsonField.Default + |> cacheResult let getJsonUnionCase: UnionCaseInfo -> JsonUnionCase = - findAttributeCase >> Option.defaultValue JsonUnionCase.Default |> cacheResult - + findAttributeCase + >> Option.defaultValue JsonUnionCase.Default + |> cacheResult + let getTransform: Type -> ITypeTransform = createTransform |> cacheResult let getJsonFieldName (config: JsonConfig) (attribute: JsonField) (prop: PropertyInfo) = @@ -44,15 +55,20 @@ module internal Core = | null -> config.jsonFieldNaming prop.Name | fieldName -> fieldName - let getJsonUnionCaseName (config: JsonConfig) (jsonUnion: JsonUnion) (jsonUnionCase: JsonUnionCase) (caseInfo: UnionCaseInfo) = + let getJsonUnionCaseName + (config: JsonConfig) + (jsonUnion: JsonUnion) + (jsonUnionCase: JsonUnionCase) + (caseInfo: UnionCaseInfo) + = match jsonUnionCase.Case with - | null -> + | null -> match jsonUnion.Mode with | UnionMode.CaseKeyAsFieldName -> config.jsonFieldNaming caseInfo.Name | _ -> caseInfo.Name | value -> value - let transformToTargetType (t: Type) (value: obj) (transform: Type): (Type*obj) = + let transformToTargetType (t: Type) (value: obj) (transform: Type) : (Type * obj) = match transform with | null -> (t, value) | converterType -> @@ -67,15 +83,16 @@ module internal Core = match config.enumValue with | EnumMode.Default -> EnumMode.Name | m -> m - | m -> m + | m -> m let failSerialization (message: string) = raise (new JsonSerializationError(message)) - let rec serialize (config: JsonConfig) (t: Type) (value: obj): JsonValue = - let serializeEnum (t: Type) (jsonField: JsonField) (value: obj): JsonValue = + let rec serialize (config: JsonConfig) (t: Type) (value: obj) : JsonValue = + let serializeEnum (t: Type) (jsonField: JsonField) (value: obj) : JsonValue = let baseT = Enum.GetUnderlyingType t let enumMode = getEnumMode config jsonField + match enumMode with | EnumMode.Value -> match baseT with @@ -91,168 +108,208 @@ module internal Core = | EnumMode.Name -> let strvalue = Enum.GetName(t, value) JsonValue.String strvalue - | mode -> failSerialization <| sprintf "Failed to serialize enum %s, unsupported enum mode: %A" t.Name mode + | mode -> + failSerialization + <| sprintf "Failed to serialize enum %s, unsupported enum mode: %A" t.Name mode - let getUntypedType (t: Type) (value: obj): Type = + let getUntypedType (t: Type) (value: obj) : Type = if t = typeof then if config.allowUntyped then value.GetType() else - failSerialization <| "Failed to serialize untyped data, allowUntyped set to false" - else t + failSerialization + <| "Failed to serialize untyped data, allowUntyped set to false" + else + t - let serializeNonOption (t: Type) (jsonField: JsonField) (value: obj): JsonValue = + let serializeNonOption (t: Type) (jsonField: JsonField) (value: obj) : JsonValue = match jsonField.AsJson with | false -> - let t, value = transformToTargetType t value jsonField.Transform + let t, value = + transformToTargetType t value jsonField.Transform + let t = getUntypedType t value + match t with - | t when t = typeof -> - JsonValue.Number (decimal (value :?> uint16)) - | t when t = typeof -> - JsonValue.Number (decimal (value :?> int16)) - | t when t = typeof -> - JsonValue.Number (decimal (value :?> int)) - | t when t = typeof -> - JsonValue.Number (decimal (value :?> uint32)) - | t when t = typeof -> - JsonValue.Number (decimal (value :?> int64)) - | t when t = typeof -> - JsonValue.Number (decimal (value :?> uint64)) - | t when t = typeof -> - JsonValue.Number (decimal (value :?> bigint)) - | t when t = typeof -> - JsonValue.Float (float (value :?> single)) - | t when t = typeof -> - JsonValue.Float (value :?> float) - | t when t = typeof -> - JsonValue.Number (value :?> decimal) - | t when t = typeof -> - JsonValue.Number (decimal (value :?> byte)) - | t when t = typeof -> - JsonValue.Number (decimal (value :?> sbyte)) - | t when t = typeof -> - JsonValue.Boolean (value :?> bool) - | t when t = typeof -> - JsonValue.String (value :?> string) - | t when t = typeof -> - JsonValue.String (string(value :?> char)) + | t when t = typeof -> JsonValue.Number(decimal (value :?> uint16)) + | t when t = typeof -> JsonValue.Number(decimal (value :?> int16)) + | t when t = typeof -> JsonValue.Number(decimal (value :?> int)) + | t when t = typeof -> JsonValue.Number(decimal (value :?> uint32)) + | t when t = typeof -> JsonValue.Number(decimal (value :?> int64)) + | t when t = typeof -> JsonValue.Number(decimal (value :?> uint64)) + | t when t = typeof -> JsonValue.Number(decimal (value :?> bigint)) + | t when t = typeof -> JsonValue.Float(float (value :?> single)) + | t when t = typeof -> JsonValue.Float(value :?> float) + | t when t = typeof -> JsonValue.Number(value :?> decimal) + | t when t = typeof -> JsonValue.Number(decimal (value :?> byte)) + | t when t = typeof -> JsonValue.Number(decimal (value :?> sbyte)) + | t when t = typeof -> JsonValue.Boolean(value :?> bool) + | t when t = typeof -> JsonValue.String(value :?> string) + | t when t = typeof -> JsonValue.String(string (value :?> char)) | t when t = typeof -> - JsonValue.String ((value :?> DateTime).ToString(jsonField.DateTimeFormat)) + JsonValue.String( + (value :?> DateTime) + .ToString(jsonField.DateTimeFormat) + ) | t when t = typeof -> - JsonValue.String ((value :?> DateTimeOffset).ToString(jsonField.DateTimeFormat)) - | t when t = typeof -> - JsonValue.String ((value :?> Guid).ToString()) - | t when t.IsEnum -> - serializeEnum t jsonField value - | t when isTuple t || isList t || isArray t || isMap t || isRecord t || isUnion t -> + JsonValue.String( + (value :?> DateTimeOffset) + .ToString(jsonField.DateTimeFormat) + ) + | t when t = typeof -> JsonValue.String((value :?> Guid).ToString()) + | t when t.IsEnum -> serializeEnum t jsonField value + | t when + isTuple t + || isList t + || isArray t + || isMap t + || isRecord t + || isUnion t + || isSet t + || isResizeArray t + -> serialize config t value - | _ -> failSerialization <| sprintf "Unknown type: %s" t.Name + | _ -> + failSerialization + <| sprintf "Unknown type: %s" t.Name | true -> let value = value :?> string + try JsonValue.Parse value - with ex -> - JsonValue.String value - - let serializeUnwrapOption (t: Type) (jsonField: JsonField) (value: obj): JsonValue option = + with + | ex -> JsonValue.String value + + let serializeUnwrapOption (t: Type) (jsonField: JsonField) (value: obj) : JsonValue option = match t with - | t when isOption t -> + | t when isOption t -> let unwrapedValue = unwrapOption t value + match unwrapedValue with - | Some value -> Some (serializeNonOption (getOptionType t) jsonField value) - | None -> + | Some value -> Some(serializeNonOption (getOptionType t) jsonField value) + | None -> match config.serializeNone with | Null -> Some JsonValue.Null | Omit -> None - | _ -> Some (serializeNonOption t jsonField value) + | _ -> Some(serializeNonOption t jsonField value) - let serializeUnwrapOptionWithNull (t: Type) (jsonField: JsonField) (value: obj): JsonValue = + let serializeUnwrapOptionWithNull (t: Type) (jsonField: JsonField) (value: obj) : JsonValue = match t with - | t when isOption t -> + | t when isOption t -> let unwrapedValue = unwrapOption t value + match unwrapedValue with | Some value -> serializeNonOption (getOptionType t) jsonField value | None -> JsonValue.Null | _ -> serializeNonOption t jsonField value - let serializeProperty (therec: obj) (prop: PropertyInfo): (string*JsonValue) option = + let serializeProperty (therec: obj) (prop: PropertyInfo) : (string * JsonValue) option = let jsonField = getJsonFieldProperty prop let propValue = prop.GetValue(therec, Array.empty) let name = getJsonFieldName config jsonField prop - let jvalue = serializeUnwrapOption prop.PropertyType jsonField propValue + + let jvalue = + serializeUnwrapOption prop.PropertyType jsonField propValue + match jvalue with - | Some jvalue -> Some (name, jvalue) + | Some jvalue -> Some(name, jvalue) | None -> None - let serializeEnumerable (values: IEnumerable): JsonValue = + let serializeEnumerable (values: IEnumerable) : JsonValue = let items = values.Cast() - |> Seq.map (fun value -> - serializeUnwrapOption (value.GetType()) JsonField.Default value) + |> Seq.map (fun value -> serializeUnwrapOption (value.GetType()) JsonField.Default value) |> Seq.map (Option.defaultValue JsonValue.Null) + items |> Array.ofSeq |> JsonValue.Array - let serializeTupleItems (types: Type seq) (values: IEnumerable): JsonValue = + let serializeTupleItems (types: Type seq) (values: IEnumerable) : JsonValue = let items = values.Cast() |> Seq.zip types - |> Seq.map (fun (t, value) -> - serializeUnwrapOption t JsonField.Default value) + |> Seq.map (fun (t, value) -> serializeUnwrapOption t JsonField.Default value) |> Seq.map (Option.defaultValue JsonValue.Null) + items |> Array.ofSeq |> JsonValue.Array - let serializeKvpEnumerable (kvps: IEnumerable): JsonValue = + let serializeKvpEnumerable (kvps: IEnumerable) : JsonValue = let props = kvps.Cast() |> Seq.map (fun kvp -> let key = KvpKey kvp :?> string let value = KvpValue kvp - let jvalue = serializeUnwrapOption (value.GetType()) JsonField.Default value - (key, Option.defaultValue JsonValue.Null jvalue) - ) - props|> Array.ofSeq |> JsonValue.Record - - let serializeRecord (t: Type) (therec: obj): JsonValue = - let props: PropertyInfo array = getRecordFields(t) - let fields = props |> Array.map (serializeProperty therec) |> Array.choose id + + let jvalue = + match value with + | null -> None + | value -> serializeUnwrapOption (value.GetType()) JsonField.Default value + + (key, Option.defaultValue JsonValue.Null jvalue)) + + props |> Array.ofSeq |> JsonValue.Record + + let serializeRecord (t: Type) (therec: obj) : JsonValue = + let props: PropertyInfo array = getRecordFields (t) + + let fields = + props + |> Array.map (serializeProperty therec) + |> Array.choose id + JsonValue.Record fields - let serializeUnion (t: Type) (theunion: obj): JsonValue = + let serializeUnion (t: Type) (theunion: obj) : JsonValue = let caseInfo, values = FSharpValue.GetUnionFields(theunion, t) let jsonUnionCase = getJsonUnionCase caseInfo let jsonUnion = getJsonUnion caseInfo.DeclaringType - let theCase = getJsonUnionCaseName config jsonUnion jsonUnionCase caseInfo + + let theCase = + getJsonUnionCaseName config jsonUnion jsonUnionCase caseInfo match values.Length with | 0 -> JsonValue.String theCase | _ -> let jsonField = getJsonFieldUnionCase caseInfo - let types = caseInfo.GetFields() |> Array.map (fun p -> p.PropertyType) + + let types = + caseInfo.GetFields() + |> Array.map (fun p -> p.PropertyType) + let jvalue = match values.Length with | 1 -> let caseValue = values.[0] let caseType = types.[0] serializeUnwrapOptionWithNull caseType jsonField caseValue - | _ -> - serializeTupleItems types values + | _ -> serializeTupleItems types values + let unionCases = getUnionCases caseInfo.DeclaringType + match unionCases.Length with | 1 -> jvalue | _ -> match jsonUnion.Mode with | UnionMode.CaseKeyAsFieldName -> JsonValue.Record [| (theCase, jvalue) |] | UnionMode.CaseKeyAsFieldValue -> - let jkey = (jsonUnion.CaseKeyField, JsonValue.String theCase) + let jkey = + (jsonUnion.CaseKeyField, JsonValue.String theCase) + let jvalue = (jsonUnion.CaseValueField, jvalue) JsonValue.Record [| jkey; jvalue |] | UnionMode.CaseKeyDiscriminatorField -> match jvalue with | JsonValue.Record jrecord -> - JsonValue.Record (Array.append [| (jsonUnion.CaseKeyField, JsonValue.String theCase) |] jrecord) - | _ -> failSerialization <| sprintf "Failed to serialize union, union mode %A supports only objects as discriminated field could exist only in object" jsonUnion.Mode - | mode -> failSerialization <| sprintf "Failed to serialize union, unsupported union mode: %A" mode + JsonValue.Record( + Array.append [| (jsonUnion.CaseKeyField, JsonValue.String theCase) |] jrecord + ) + | _ -> + failSerialization + <| sprintf + "Failed to serialize union, union mode %A supports only objects as discriminated field could exist only in object" + jsonUnion.Mode + | mode -> + failSerialization + <| sprintf "Failed to serialize union, unsupported union mode: %A" mode match t with | t when isRecord t -> serializeRecord t value @@ -261,25 +318,33 @@ module internal Core = | t when isList t -> serializeEnumerable (value :?> IEnumerable) | t when isTuple t -> serializeTupleItems (getTupleElements t) (FSharpValue.GetTupleFields value) | t when isUnion t -> serializeUnion t value + | t when isSet t -> serializeEnumerable (value :?> IEnumerable) + | t when isResizeArray t -> serializeEnumerable (value :?> IEnumerable) | t -> - let msg = sprintf "Failed to serialize, must be one of following types: record, map, array, list, tuple, union. Type is: %s." t.Name - failSerialization msg - + let msg = + sprintf + "Failed to serialize, must be one of following types: record, map, array, list, tuple, union. Type is: %s." + t.Name + + failSerialization msg + let failDeserialization (path: JsonPath) (message: string) = - let message = sprintf "JSON Path: %s. %s" (path.toString()) message + let message = + sprintf "JSON Path: %s. %s" (path.toString ()) message + raise (new JsonDeserializationError(path, message)) - - let getTargetType (t: Type) (jsonField: JsonField): Type = + + let getTargetType (t: Type) (jsonField: JsonField) : Type = match jsonField.Transform with | null -> t | transformType -> (getTransform transformType).targetType () - let transformFromTargetType (transform: Type) (value: obj): obj = + let transformFromTargetType (transform: Type) (value: obj) : obj = match transform with | null -> value | converterType -> (getTransform converterType).fromTargetType value - let getJsonValueType (jvalue: JsonValue): Type = + let getJsonValueType (jvalue: JsonValue) : Type = match jvalue with | JsonValue.String _ -> typeof | JsonValue.Number _ -> typeof @@ -288,11 +353,12 @@ module internal Core = | JsonValue.Array _ -> getListType typeof | JsonValue.Boolean _ -> typeof | _ -> null - - let rec deserialize (config: JsonConfig) (path: JsonPath) (t: Type) (jvalue: JsonValue): obj = - let deserializeEnum (path: JsonPath) (t: Type) (jsonField: JsonField) (jvalue: JsonValue): obj = + + let rec deserialize (config: JsonConfig) (path: JsonPath) (t: Type) (jvalue: JsonValue) : obj = + let deserializeEnum (path: JsonPath) (t: Type) (jsonField: JsonField) (jvalue: JsonValue) : obj = let baseT = Enum.GetUnderlyingType t let enumMode = getEnumMode config jsonField + match enumMode with | EnumMode.Value -> match baseT with @@ -308,18 +374,21 @@ module internal Core = | EnumMode.Name -> let valueStr = JsonValueHelpers.getString path jvalue Enum.Parse(t, valueStr) - | mode -> failDeserialization path <| sprintf "Failed to deserialize enum %s, unsupported enum mode: %A" t.Name mode + | mode -> + failDeserialization path + <| sprintf "Failed to deserialize enum %s, unsupported enum mode: %A" t.Name mode - let getUntypedType (path: JsonPath) (t: Type) (jvalue: JsonValue): Type = + let getUntypedType (path: JsonPath) (t: Type) (jvalue: JsonValue) : Type = match t with | t when t = typeof -> if config.allowUntyped then getJsonValueType jvalue else - failDeserialization path <| sprintf "Failed to deserialize object, allowUntyped set to false" - | t -> t + failDeserialization path + <| sprintf "Failed to deserialize object, allowUntyped set to false" + | t -> t - let deserializeNonOption (path: JsonPath) (t: Type) (jsonField: JsonField) (jvalue: JsonValue): obj = + let deserializeNonOption (path: JsonPath) (t: Type) (jsonField: JsonField) (jvalue: JsonValue) : obj = match jsonField.AsJson with | true -> match jvalue with @@ -328,212 +397,314 @@ module internal Core = | false -> let t = getTargetType t jsonField let t = getUntypedType path t jvalue + let jvalue = match t with - | t when t = typeof -> - JsonValueHelpers.getInt16 path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getUInt16 path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getInt path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getUInt32 path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getInt64 path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getUInt64 path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getBigint path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getSingle path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getFloat path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getDecimal path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getByte path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getSByte path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getBool path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getString path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getChar path jvalue :> obj + | t when isNull t -> null :> obj + | t when t = typeof -> JsonValueHelpers.getInt16 path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getUInt16 path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getInt path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getUInt32 path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getInt64 path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getUInt64 path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getBigint path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getSingle path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getFloat path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getDecimal path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getByte path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getSByte path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getBool path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getString path jvalue :> obj + | t when t = typeof -> JsonValueHelpers.getChar path jvalue :> obj | t when t = typeof -> JsonValueHelpers.getDateTime CultureInfo.InvariantCulture path jvalue :> obj | t when t = typeof -> JsonValueHelpers.getDateTimeOffset CultureInfo.InvariantCulture path jvalue :> obj - | t when t = typeof -> - JsonValueHelpers.getGuid path jvalue :> obj - | t when t.IsEnum -> - deserializeEnum path t jsonField jvalue - | t when isTuple t || isList t || isArray t || isMap t || isRecord t || isUnion t -> + | t when t = typeof -> JsonValueHelpers.getGuid path jvalue :> obj + | t when t.IsEnum -> deserializeEnum path t jsonField jvalue + | t when + isTuple t + || isList t + || isArray t + || isMap t + || isRecord t + || isUnion t + || isSet t + || isResizeArray t + -> deserialize config path t jvalue - | _ -> failDeserialization path <| sprintf "Not supported type: %s" t.Name + | _ -> + failDeserialization path + <| sprintf "Not supported type: %s" t.Name + transformFromTargetType jsonField.Transform jvalue - let deserializeUnwrapOption (path: JsonPath) (t: Type) (jsonField: JsonField) (jvalue: JsonValue option): obj = + let deserializeUnwrapOption (path: JsonPath) (t: Type) (jsonField: JsonField) (jvalue: JsonValue option) : obj = match t with | t when isOption t -> match jvalue with | Some jvalue -> match jvalue with | JsonValue.Null -> optionNone t - | _ -> deserializeNonOption path (getOptionType t) jsonField jvalue |> optionSome t + | _ -> + deserializeNonOption path (getOptionType t) jsonField jvalue + |> optionSome t | None -> match config.deserializeOption with | RequireNull -> - failDeserialization path "Option field is not found while using RequireNull option deserialization" - | AllowOmit -> - optionNone t + failDeserialization + path + "Option field is not found while using RequireNull option deserialization" + | AllowOmit -> optionNone t | _ -> match jvalue with - | Some jvalue -> - deserializeNonOption path t jsonField jvalue + | Some jvalue -> deserializeNonOption path t jsonField jvalue | None -> match jsonField.DefaultValue with | null -> failDeserialization path "Non option field is missing" | defaultValue -> defaultValue - let deserializeMap (path: JsonPath) (t: Type) (jvalue: JsonValue): obj = + let deserializeMap (path: JsonPath) (t: Type) (jvalue: JsonValue) : obj = let itemValueType = getMapValueType t + match jvalue with | JsonValue.Record fields -> - fields + fields |> Array.map (fun field -> let itemName = fst field let itemJsonValue = snd field - let itemPath = JsonPathItem.Field itemName |> path.createNew - let itemValue = deserializeUnwrapOption itemPath itemValueType JsonField.Default (Some itemJsonValue) - (itemName, itemValue) - ) - |> List.ofArray |> CreateMap t - | _ -> failDeserialization path "Failed to parse map from JSON that is not object." + + let itemPath = + JsonPathItem.Field itemName |> path.createNew + + let itemValue = + deserializeUnwrapOption itemPath itemValueType JsonField.Default (Some itemJsonValue) + + (itemName, itemValue)) + |> List.ofArray + |> CreateMap t + | _ -> failDeserialization path "Failed to parse map from JSON that is not object." let deserializeArrayItems (path: JsonPath) (t: Type) (jvalues: JsonValue array) = - jvalues |> Array.mapi (fun index jvalue -> - let itemPath = JsonPathItem.ArrayItem index |> path.createNew - deserializeUnwrapOption itemPath t JsonField.Default (Some jvalue) - ) + jvalues + |> Array.mapi (fun index jvalue -> + let itemPath = + JsonPathItem.ArrayItem index |> path.createNew - let deserializeList (path: JsonPath) (t: Type) (jvalue: JsonValue): obj = + deserializeUnwrapOption itemPath t JsonField.Default (Some jvalue)) + + let deserializeList (path: JsonPath) (t: Type) (jvalue: JsonValue) : obj = match jvalue with | JsonValue.Array jvalues -> let itemType = getListItemType t - let arrayValues = deserializeArrayItems path itemType jvalues + + let arrayValues = + deserializeArrayItems path itemType jvalues + arrayValues |> List.ofSeq |> createList itemType | _ -> failDeserialization path "Failed to parse list from JSON that is not array." - let deserializeArray (path: JsonPath) (t: Type) (jvalue: JsonValue): obj = + let deserializeSet (path: JsonPath) (t: Type) (jvalue: JsonValue) : obj = + match jvalue with + | JsonValue.Array jvalues -> + let itemType = getSetItemType t + + let arrayValues = + deserializeArrayItems path itemType jvalues + + arrayValues |> List.ofSeq |> createSet itemType + | _ -> failDeserialization path "Failed to parse set from JSON that is not array." + + let deserializeResizeArray (path: JsonPath) (t: Type) (jvalue: JsonValue) : obj = + match jvalue with + | JsonValue.Array jvalues -> + let itemType = getResizeArrayItemType t + + let arrayValues = + deserializeArrayItems path itemType jvalues + + arrayValues + |> List.ofSeq + |> createResizeArray itemType + | _ -> failDeserialization path "Failed to parse resize array from JSON that is not array." + + let deserializeArray (path: JsonPath) (t: Type) (jvalue: JsonValue) : obj = match jvalue with | JsonValue.Array jvalues -> let itemType = t.GetElementType() - let arrayValues = deserializeArrayItems path itemType jvalues - let arr = Array.CreateInstance(itemType, arrayValues.Length) - arrayValues |> Array.iteri (fun index value -> arr.SetValue(value, index)) + + let arrayValues = + deserializeArrayItems path itemType jvalues + + let arr = + Array.CreateInstance(itemType, arrayValues.Length) + + arrayValues + |> Array.iteri (fun index value -> arr.SetValue(value, index)) + arr :> obj | _ -> failDeserialization path "Failed to parse array from JSON that is not array." - let deserializeTupleElements (path: JsonPath) (types: Type[]) (jvalue: JsonValue): obj[] = + let deserializeTupleElements (path: JsonPath) (types: Type []) (jvalue: JsonValue) : obj [] = match jvalue with | JsonValue.Array values -> if types.Length <> values.Length then - failDeserialization path "Failed to parse tuple. Number of values in JSON list does not match number of elements in tuple." - let tupleValues = (Array.zip types values) |> Array.mapi (fun index (t, value) -> - let itemPath = JsonPathItem.ArrayItem index |> path.createNew - deserializeUnwrapOption itemPath t JsonField.Default (Some value) - ) + failDeserialization + path + "Failed to parse tuple. Number of values in JSON list does not match number of elements in tuple." + + let tupleValues = + (Array.zip types values) + |> Array.mapi (fun index (t, value) -> + let itemPath = + JsonPathItem.ArrayItem index |> path.createNew + + deserializeUnwrapOption itemPath t JsonField.Default (Some value)) + tupleValues | _ -> failDeserialization path "Failed to parse tuple from JSON that is not array." - let deserializeTuple (path: JsonPath) (t: Type) (jvalue: JsonValue): obj = + let deserializeTuple (path: JsonPath) (t: Type) (jvalue: JsonValue) : obj = let types = getTupleElements t - let tupleValues = deserializeTupleElements path types jvalue - FSharpValue.MakeTuple (tupleValues, t) - let deserializeProperty (path: JsonPath) (fields: (string*JsonValue) array) (prop: PropertyInfo): obj = + let tupleValues = + deserializeTupleElements path types jvalue + + FSharpValue.MakeTuple(tupleValues, t) + + let deserializeProperty (path: JsonPath) (fields: (string * JsonValue) array) (prop: PropertyInfo) : obj = let jsonField = getJsonFieldProperty prop let name = getJsonFieldName config jsonField prop - let field = fields |> Seq.tryFind (fun f -> fst f = name) + + let field = + fields |> Seq.tryFind (fun f -> fst f = name) + let fieldValue = field |> Option.map snd - let propPath = JsonPathItem.Field name |> path.createNew + + let propPath = + JsonPathItem.Field name |> path.createNew + deserializeUnwrapOption propPath prop.PropertyType jsonField fieldValue - let deserializeRecord (path: JsonPath) (t: Type) (jvalue: JsonValue): obj = + let deserializeRecord (path: JsonPath) (t: Type) (jvalue: JsonValue) : obj = match jvalue with | JsonValue.Record fields -> let props: PropertyInfo array = getRecordFields t - let propsValues = props |> Array.map (deserializeProperty path fields) + + let propsValues = + props + |> Array.map (deserializeProperty path fields) + FSharpValue.MakeRecord(t, propsValues) | _ -> failDeserialization path "Failed to parse record from JSON that is not object." - let getUnionCaseInfo (path: JsonPath) (t: Type) (jCaseName: string): UnionCaseInfo = + let getUnionCaseInfo (path: JsonPath) (t: Type) (jCaseName: string) : UnionCaseInfo = let jsonUnion = t |> getJsonUnion let unionCases = t |> getUnionCases - let caseInfo = unionCases |> Array.tryFind (fun c -> getJsonUnionCaseName config jsonUnion (getJsonUnionCase c) c = jCaseName) + + let caseInfo = + unionCases + |> Array.tryFind (fun c -> getJsonUnionCaseName config jsonUnion (getJsonUnionCase c) c = jCaseName) + match caseInfo with | Some caseInfo -> caseInfo - | None -> failDeserialization path <| sprintf "Failed to parse union, unable to find union case: %s." jCaseName - - let mustFindField (path: JsonPath) (fieldName: string) (fields: (string * JsonValue)[]): string * JsonValue = - let caseKeyField = fields |> Seq.tryFind (fun f -> fst f = fieldName) + | None -> + failDeserialization path + <| sprintf "Failed to parse union, unable to find union case: %s." jCaseName + + let mustFindField (path: JsonPath) (fieldName: string) (fields: (string * JsonValue) []) : string * JsonValue = + let caseKeyField = + fields |> Seq.tryFind (fun f -> fst f = fieldName) + match caseKeyField with | Some field -> field - | None -> failDeserialization path <| sprintf "Failed to parse union, unable to find field: %s." fieldName + | None -> + failDeserialization path + <| sprintf "Failed to parse union, unable to find field: %s." fieldName - let makeUnion (path: JsonPath) (t: Type) (jCaseName: string) (jCaseValue: JsonValue): obj = + let makeUnion (path: JsonPath) (t: Type) (jCaseName: string) (jCaseValue: JsonValue) : obj = let caseInfo = jCaseName |> getUnionCaseInfo path t - let casePath = JsonPathItem.Field jCaseName |> path.createNew + + let casePath = + JsonPathItem.Field jCaseName |> path.createNew + let props: PropertyInfo array = caseInfo.GetFields() let fieldAttr = getJsonFieldUnionCase caseInfo + let values = match props with - | [| prop |] -> - [| deserializeUnwrapOption casePath prop.PropertyType fieldAttr (Some jCaseValue) |] + | [| prop |] -> [| deserializeUnwrapOption casePath prop.PropertyType fieldAttr (Some jCaseValue) |] | _ -> - let propsTypes = props |> Array.map (fun p -> p.PropertyType) + let propsTypes = + props |> Array.map (fun p -> p.PropertyType) + deserializeTupleElements casePath propsTypes jCaseValue - FSharpValue.MakeUnion (caseInfo, values) - - let deserializeUnion (path: JsonPath) (t: Type) (jvalue: JsonValue): obj = + + FSharpValue.MakeUnion(caseInfo, values) + + let deserializeUnion (path: JsonPath) (t: Type) (jvalue: JsonValue) : obj = let jsonUnion = t |> getJsonUnion let unionCases = t |> getUnionCases + match unionCases with | [| caseInfo |] -> let fieldAttr = getJsonFieldUnionCase caseInfo let props: PropertyInfo array = caseInfo.GetFields() + let values = match props with - | [| prop |] -> - [| deserializeUnwrapOption path prop.PropertyType fieldAttr (Some jvalue) |] + | [| prop |] -> [| deserializeUnwrapOption path prop.PropertyType fieldAttr (Some jvalue) |] | _ -> - let propsTypes = props |> Array.map (fun p -> p.PropertyType) + let propsTypes = + props |> Array.map (fun p -> p.PropertyType) + deserializeTupleElements path propsTypes jvalue - FSharpValue.MakeUnion (caseInfo, values) + + FSharpValue.MakeUnion(caseInfo, values) | _ -> match jvalue with - | JsonValue.String caseName -> - FSharpValue.MakeUnion (caseName |> getUnionCaseInfo path t, null) + | JsonValue.String caseName -> FSharpValue.MakeUnion(caseName |> getUnionCaseInfo path t, null) | JsonValue.Record fields -> match jsonUnion.Mode with | UnionMode.CaseKeyDiscriminatorField -> - let caseKeyFieldName, caseKeyFieldValue = mustFindField path jsonUnion.CaseKeyField fields - let caseNamePath = caseKeyFieldName |> JsonPathItem.Field |> path.createNew - let jCaseName = caseKeyFieldValue |> JsonValueHelpers.getString caseNamePath + let caseKeyFieldName, caseKeyFieldValue = + mustFindField path jsonUnion.CaseKeyField fields + + let caseNamePath = + caseKeyFieldName + |> JsonPathItem.Field + |> path.createNew + + let jCaseName = + caseKeyFieldValue + |> JsonValueHelpers.getString caseNamePath + makeUnion path t jCaseName jvalue | UnionMode.CaseKeyAsFieldValue -> - let caseKeyFieldName, caseKeyFieldValue = mustFindField path jsonUnion.CaseKeyField fields - let _, jCaseValue = mustFindField path jsonUnion.CaseValueField fields - let caseNamePath = caseKeyFieldName |> JsonPathItem.Field |> path.createNew - let jCaseName = caseKeyFieldValue |> JsonValueHelpers.getString caseNamePath + let caseKeyFieldName, caseKeyFieldValue = + mustFindField path jsonUnion.CaseKeyField fields + + let _, jCaseValue = + mustFindField path jsonUnion.CaseValueField fields + + let caseNamePath = + caseKeyFieldName + |> JsonPathItem.Field + |> path.createNew + + let jCaseName = + caseKeyFieldValue + |> JsonValueHelpers.getString caseNamePath + makeUnion path t jCaseName jCaseValue | UnionMode.CaseKeyAsFieldName -> match fields with - | [| (jCaseName, jCaseValue) |] -> - makeUnion path t jCaseName jCaseValue + | [| (jCaseName, jCaseValue) |] -> makeUnion path t jCaseName jCaseValue | _ -> - failDeserialization path <| sprintf "Failed to parse union from record with %i fields, should be 1 field." fields.Length + failDeserialization path + <| sprintf + "Failed to parse union from record with %i fields, should be 1 field." + fields.Length | _ -> failDeserialization path "Failed to parse union from JSON that is not object." match t with @@ -543,5 +714,10 @@ module internal Core = | t when isList t -> deserializeList path t jvalue | t when isTuple t -> deserializeTuple path t jvalue | t when isUnion t -> deserializeUnion path t jvalue - | _ -> failDeserialization path <| sprintf "Failed to serialize, must be one of following types: record, map, array, list, tuple, union. Type is: %s." t.Name - \ No newline at end of file + | t when isSet t -> deserializeSet path t jvalue + | t when isResizeArray t -> deserializeResizeArray path t jvalue + | _ -> + failDeserialization path + <| sprintf + "Failed to serialize, must be one of following types: record, map, array, list, set, tuple, union. Type is: %s." + t.Name diff --git a/FSharp.Json/FSharp.Json.fsproj b/FSharp.Json/FSharp.Json.fsproj index ff632bb..e5802b1 100644 --- a/FSharp.Json/FSharp.Json.fsproj +++ b/FSharp.Json/FSharp.Json.fsproj @@ -1,28 +1,19 @@ - - - - netstandard2.0 - F# JSON Reflection based serialization library - vsapronov - https://github.com/vsapronov/FSharp.Json - Copyright 2019 - F# JSON serialization - https://github.com/vsapronov/FSharp.Json - 0.4.1 - - - - - - - - - - - - - - - - - + + + + netstandard2.0 + NicoVIII.FSharp.Json + + + + + + + + + + + + + + diff --git a/FSharp.Json/Interface.fs b/FSharp.Json/Interface.fs index 4f89d65..1760e09 100644 --- a/FSharp.Json/Interface.fs +++ b/FSharp.Json/Interface.fs @@ -3,39 +3,58 @@ /// Contains main API functions module Json = open System - open System.Text.RegularExpressions + open System.Text.RegularExpressions open FSharp.Json.Internalized.FSharp.Data let internal toLower (str: string) = str.ToLower() - let internal firstCharCapital (str: string) = (str.[0].ToString()).ToUpper() + str.Substring(1) + let internal firstCharCapital (str: string) = + (str.[0].ToString()).ToUpper() + str.Substring(1) /// Converts names into lower camel case. Use in [JsonConfig]. - let lowerCamelCase (name: string): string = - let regex = @"(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])" - let words = Regex.Split(name, regex) |> List.ofArray |> List.map toLower + let lowerCamelCase (name: string) : string = + let regex = + @"(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])" + + let words = + Regex.Split(name, regex) + |> List.ofArray + |> List.map toLower + let first = List.head words - let tail = List.tail words |> List.map firstCharCapital - let parts = [first.ToLower()] @ tail + + let tail = + List.tail words |> List.map firstCharCapital + + let parts = [ first.ToLower() ] @ tail String.Join("", parts) /// Converts names into snake case. Use in [JsonConfig]. - let snakeCase (name: string): string = - let regex = @"(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])" - let words = Regex.Split(name, regex) |> List.ofArray |> List.map toLower + let snakeCase (name: string) : string = + let regex = + @"(?<=[A-Z])(?=[A-Z][a-z])|(?<=[^A-Z])(?=[A-Z])|(?<=[A-Za-z])(?=[^A-Za-z])" + + let words = + Regex.Split(name, regex) + |> List.ofArray + |> List.map toLower + String.Join("_", words) /// Serailizes F# object into JSON. Uses provided [JsonConfig]. - let serializeEx (config: JsonConfig) (theobj: obj): string = - let json = Core.serialize config (theobj.GetType()) theobj + let serializeEx (config: JsonConfig) (theobj: obj) : string = + let json = + Core.serialize config (theobj.GetType()) theobj + let saveOptions = match config.unformatted with | true -> JsonSaveOptions.DisableFormatting | false -> JsonSaveOptions.None + json.ToString(saveOptions) /// Deserailizes JSON into F# type provided as generic parameter. Uses provided [JsonConfig]. - let deserializeEx<'T> (config: JsonConfig) (json: string): 'T = + let deserializeEx<'T> (config: JsonConfig) (json: string) : 'T = let value = JsonValue.Parse(json) (Core.deserialize config JsonPath.Root typeof<'T> value) :?> 'T @@ -44,8 +63,9 @@ module Json = /// Serailizes F# object into unformatted JSON. Uses default [JsonConfig]. let serializeU (theobj: obj) = - let config = JsonConfig.create(unformatted = true) + let config = JsonConfig.create (unformatted = true) serializeEx config theobj /// Deserailizes JSON into F# type provided as generic parameter. Uses default [JsonConfig]. - let deserialize<'T> (json: string) = deserializeEx<'T> JsonConfig.Default json + let deserialize<'T> (json: string) = + deserializeEx<'T> JsonConfig.Default json diff --git a/FSharp.Json/InterfaceTypes.fs b/FSharp.Json/InterfaceTypes.fs index c8ce7a5..d0bea87 100644 --- a/FSharp.Json/InterfaceTypes.fs +++ b/FSharp.Json/InterfaceTypes.fs @@ -6,11 +6,11 @@ open System.Text /// Transformation between types type ITypeTransform = /// Represents target type to transform to - abstract member targetType: unit -> Type + abstract member targetType : unit -> Type /// Transform to target type - abstract member toTargetType: obj -> obj + abstract member toTargetType : obj -> obj /// Transform from target type - abstract member fromTargetType: obj -> obj + abstract member fromTargetType : obj -> obj /// Controls Enum serialization type EnumMode = @@ -21,8 +21,8 @@ type EnumMode = /// Serialize Enum value as it's value | Value = 2 -/// Attribute customizing serialization of members -type JsonField (name: string) = +/// Attribute customizing serialization of members +type JsonField(name: string) = inherit Attribute() /// Field name in JSON. Use null value to use record member name. Default value is null. member val public Name: string = name with get, set @@ -37,10 +37,10 @@ type JsonField (name: string) = /// Controls default DateTime and DateTimeOffset formats. Default value is "yyyy-MM-ddTHH:mm:ss.fffffffK". member val public DateTimeFormat: string = "yyyy-MM-ddTHH:mm:ss.fffffffK" with get, set /// Creates default [JsonField] instance - new () = JsonField(null) -with - /// Default [JsonField]. - static member Default = JsonField() + new() = JsonField(null) + with + /// Default [JsonField]. + static member Default = JsonField() /// Attribute customizing serialization of union types type UnionMode = @@ -52,7 +52,7 @@ type UnionMode = | CaseKeyDiscriminatorField = 2 /// Attribute customizing serialization of union types -type JsonUnion () = +type JsonUnion() = inherit Attribute() /// Controls how to serialize cases of union type member val public Mode: UnionMode = UnionMode.CaseKeyAsFieldName with get, set @@ -60,20 +60,20 @@ type JsonUnion () = member val public CaseKeyField: string = "case" with get, set /// Field name used for case value. Applicable only when Mode set to CaseKeyAsFieldValue. Default value is "value". member val public CaseValueField: string = "value" with get, set -with - /// Default [JsonUnion]. - static member Default = JsonUnion() + with + /// Default [JsonUnion]. + static member Default = JsonUnion() /// Attribute customizing serialization of union cases -type JsonUnionCase (case: string) = +type JsonUnionCase(case: string) = inherit Attribute() /// Value that should be used for attributed union case. Default is null which means use F# case name. member val public Case: string = case with get, set /// Creates new [JsonUnionCase] - new () = JsonUnionCase(null) -with - /// Default [JsonUnion]. - static member Default = JsonUnionCase() + new() = JsonUnionCase(null) + with + /// Default [JsonUnion]. + static member Default = JsonUnionCase() /// Represents one item in [JsonPath] @@ -84,28 +84,32 @@ type JsonPathItem = | ArrayItem of int /// Represents path in JSON structure -type JsonPath = { - /// Path represented as list of [JsonPathItem]. - path: JsonPathItem list -} -with +type JsonPath = + { + /// Path represented as list of [JsonPathItem]. + path: JsonPathItem list } /// JSON root path. static member Root = { JsonPath.path = [] } /// Creates new path from this by adding item to the end. - member this.createNew (item: JsonPathItem) = { JsonPath.path = this.path @ [item] } + member this.createNew(item: JsonPathItem) = + { JsonPath.path = this.path @ [ item ] } /// Returns string representing JSON path. - member this.toString () = + member this.toString() = match this.path.Length with | 0 -> "" | _ -> let value = new StringBuilder() - this.path |> List.iteri (fun index location -> + + this.path + |> List.iteri (fun index location -> match location with | Field theProperty -> - if index <> 0 then value.Append "." |> ignore + if index <> 0 then + value.Append "." |> ignore + value.Append theProperty |> ignore - | ArrayItem item -> item |> sprintf "[%i]" |> value.Append |> ignore - ) + | ArrayItem item -> item |> sprintf "[%i]" |> value.Append |> ignore) + value.ToString() /// Fatal error during JSON serialization @@ -136,31 +140,35 @@ type DeserializeOption = type JsonFieldNaming = string -> string /// Configuration for JSON serialization/deserialization -type JsonConfig = { - /// Generates unformatted JSON if set to true. Default is false. - unformatted: bool - /// Controls serialization of option None value. Default is SerializeNone.Null. - serializeNone: SerializeNone - /// Controls deserialization of option types. Default is DeserializeOption.AllowOmit. - deserializeOption: DeserializeOption - /// Sets JSON fields naming strategy. Default is `id` function. - jsonFieldNaming: JsonFieldNaming - /// Allows untyped data, like obj. Default is false. - allowUntyped: bool - /// Controls serialization of enums. Default is EnumMode.Name - enumValue: EnumMode -} -with +type JsonConfig = + { + /// Generates unformatted JSON if set to true. Default is false. + unformatted: bool + /// Controls serialization of option None value. Default is SerializeNone.Null. + serializeNone: SerializeNone + /// Controls deserialization of option types. Default is DeserializeOption.AllowOmit. + deserializeOption: DeserializeOption + /// Sets JSON fields naming strategy. Default is `id` function. + jsonFieldNaming: JsonFieldNaming + /// Allows untyped data, like obj. Default is false. + allowUntyped: bool + /// Controls serialization of enums. Default is EnumMode.Name + enumValue: EnumMode } /// Creates customized [JsonConfig], each parameter corresponds to [JsonConfig] record member. - static member create (?unformatted, ?serializeNone, ?deserializeOption, ?jsonFieldNaming, ?allowUntyped, ?enumValue) = - { - JsonConfig.unformatted = defaultArg unformatted false - JsonConfig.serializeNone = defaultArg serializeNone SerializeNone.Null - JsonConfig.deserializeOption = defaultArg deserializeOption DeserializeOption.AllowOmit - JsonConfig.jsonFieldNaming = defaultArg jsonFieldNaming id - JsonConfig.allowUntyped = defaultArg allowUntyped false - JsonConfig.enumValue = defaultArg enumValue EnumMode.Name - } + static member create + ( + ?unformatted, + ?serializeNone, + ?deserializeOption, + ?jsonFieldNaming, + ?allowUntyped, + ?enumValue + ) = + { JsonConfig.unformatted = defaultArg unformatted false + JsonConfig.serializeNone = defaultArg serializeNone SerializeNone.Null + JsonConfig.deserializeOption = defaultArg deserializeOption DeserializeOption.AllowOmit + JsonConfig.jsonFieldNaming = defaultArg jsonFieldNaming id + JsonConfig.allowUntyped = defaultArg allowUntyped false + JsonConfig.enumValue = defaultArg enumValue EnumMode.Name } /// Default [JsonConfig]. - static member Default = JsonConfig.create() - \ No newline at end of file + static member Default = JsonConfig.create () diff --git a/FSharp.Json/JsonValue.fs b/FSharp.Json/JsonValue.fs index cfad05e..36bc994 100644 --- a/FSharp.Json/JsonValue.fs +++ b/FSharp.Json/JsonValue.fs @@ -19,10 +19,10 @@ open System.Text /// Specifies the formatting behaviour of JSON values [] type internal JsonSaveOptions = - /// Format (indent) the JsonValue - | None = 0 - /// Print the JsonValue in one line in a compact way - | DisableFormatting = 1 + /// Format (indent) the JsonValue + | None = 0 + /// Print the JsonValue in one line in a compact way + | DisableFormatting = 1 /// Represents a JSON value. Large numbers that do not fit in the /// Decimal type are represented using the Float case, while @@ -30,172 +30,209 @@ type internal JsonSaveOptions = [] [] type internal JsonValue = - | String of string - | Number of decimal - | Float of float - | Record of properties:(string * JsonValue)[] - | Array of elements:JsonValue[] - | Boolean of bool - | Null - - /// [omit] - [] - [] - member x._Print = x.ToString() - - /// Serializes the JsonValue to the specified System.IO.TextWriter. - member x.WriteTo (w:TextWriter, saveOptions) = - - let newLine = - if saveOptions = JsonSaveOptions.None then - fun indentation plus -> - w.WriteLine() - System.String(' ', indentation + plus) |> w.Write - else - fun _ _ -> () - - let propSep = - if saveOptions = JsonSaveOptions.None then "\": " - else "\":" - - let rec serialize indentation = function - | Null -> w.Write "null" - | Boolean b -> w.Write(if b then "true" else "false") - | Number number -> w.Write number - | Float number -> w.Write number - | String s -> - w.Write "\"" - JsonValue.JsonStringEncodeTo w s - w.Write "\"" - | Record properties -> - w.Write "{" - for i = 0 to properties.Length - 1 do - let k,v = properties.[i] - if i > 0 then w.Write "," - newLine indentation 2 - w.Write "\"" - JsonValue.JsonStringEncodeTo w k - w.Write propSep - serialize (indentation + 2) v - newLine indentation 0 - w.Write "}" - | Array elements -> - w.Write "[" - for i = 0 to elements.Length - 1 do - if i > 0 then w.Write "," - newLine indentation 2 - serialize (indentation + 2) elements.[i] - if elements.Length > 0 then - newLine indentation 0 - w.Write "]" - - serialize 0 x - - // Encode characters that are not valid in JS string. The implementation is based - // on https://github.com/mono/mono/blob/master/mcs/class/System.Web/System.Web/HttpUtility.cs - static member internal JsonStringEncodeTo (w:TextWriter) (value:string) = - if String.IsNullOrEmpty value then () - else - for i = 0 to value.Length - 1 do - let c = value.[i] - let ci = int c - if ci >= 0 && ci <= 7 || ci = 11 || ci >= 14 && ci <= 31 then - w.Write("\\u{0:x4}", ci) |> ignore - else - match c with - | '\b' -> w.Write "\\b" - | '\t' -> w.Write "\\t" - | '\n' -> w.Write "\\n" - | '\f' -> w.Write "\\f" - | '\r' -> w.Write "\\r" - | '"' -> w.Write "\\\"" - | '\\' -> w.Write "\\\\" - | _ -> w.Write c - - member x.ToString saveOptions = - let w = new StringWriter(CultureInfo.InvariantCulture) - x.WriteTo(w, saveOptions) - w.GetStringBuilder().ToString() - - override x.ToString() = x.ToString(JsonSaveOptions.None) + | String of string + | Number of decimal + | Float of float + | Record of properties: (string * JsonValue) [] + | Array of elements: JsonValue [] + | Boolean of bool + | Null + + /// [omit] + [] + [] + member x._Print = x.ToString() + + /// Serializes the JsonValue to the specified System.IO.TextWriter. + member x.WriteTo(w: TextWriter, saveOptions) = + + let newLine = + if saveOptions = JsonSaveOptions.None then + fun indentation plus -> + w.WriteLine() + System.String(' ', indentation + plus) |> w.Write + else + fun _ _ -> () + + let propSep = + if saveOptions = JsonSaveOptions.None then + "\": " + else + "\":" + + let rec serialize indentation = + function + | Null -> w.Write "null" + | Boolean b -> w.Write(if b then "true" else "false") + | Number number -> w.Write number + | Float number -> w.Write number + | String s -> + w.Write "\"" + JsonValue.JsonStringEncodeTo w s + w.Write "\"" + | Record properties -> + w.Write "{" + + for i = 0 to properties.Length - 1 do + let k, v = properties.[i] + if i > 0 then w.Write "," + newLine indentation 2 + w.Write "\"" + JsonValue.JsonStringEncodeTo w k + w.Write propSep + serialize (indentation + 2) v + + newLine indentation 0 + w.Write "}" + | Array elements -> + w.Write "[" + + for i = 0 to elements.Length - 1 do + if i > 0 then w.Write "," + newLine indentation 2 + serialize (indentation + 2) elements.[i] + + if elements.Length > 0 then + newLine indentation 0 + + w.Write "]" + + serialize 0 x + + // Encode characters that are not valid in JS string. The implementation is based + // on https://github.com/mono/mono/blob/master/mcs/class/System.Web/System.Web/HttpUtility.cs + static member internal JsonStringEncodeTo (w: TextWriter) (value: string) = + if String.IsNullOrEmpty value then + () + else + for i = 0 to value.Length - 1 do + let c = value.[i] + let ci = int c + + if ci >= 0 && ci <= 7 + || ci = 11 + || ci >= 14 && ci <= 31 then + w.Write("\\u{0:x4}", ci) |> ignore + else + match c with + | '\b' -> w.Write "\\b" + | '\t' -> w.Write "\\t" + | '\n' -> w.Write "\\n" + | '\f' -> w.Write "\\f" + | '\r' -> w.Write "\\r" + | '"' -> w.Write "\\\"" + | '\\' -> w.Write "\\\\" + | _ -> w.Write c + + member x.ToString saveOptions = + let w = + new StringWriter(CultureInfo.InvariantCulture) + + x.WriteTo(w, saveOptions) + w.GetStringBuilder().ToString() + + override x.ToString() = x.ToString(JsonSaveOptions.None) /// [omit] [] module internal JsonValue = - /// Active Pattern to view a `JsonValue.Record of (string * JsonValue)[]` as a `JsonValue.Object of Map` for - /// backwards compatibility reaons - [] - let (|Object|_|) x = - match x with - | JsonValue.Record properties -> Map.ofArray properties |> Some - | _ -> None + /// Active Pattern to view a `JsonValue.Record of (string * JsonValue)[]` as a `JsonValue.Object of Map` for + /// backwards compatibility reaons + [] + let (|Object|_|) x = + match x with + | JsonValue.Record properties -> Map.ofArray properties |> Some + | _ -> None - /// Constructor to create a `JsonValue.Record of (string * JsonValue)[]` as a `JsonValue.Object of Map` for - /// backwards compatibility reaons - [] - let Object = Map.toArray >> JsonValue.Record + /// Constructor to create a `JsonValue.Record of (string * JsonValue)[]` as a `JsonValue.Object of Map` for + /// backwards compatibility reaons + [] + let Object = Map.toArray >> JsonValue.Record // -------------------------------------------------------------------------------------- // JSON parser // -------------------------------------------------------------------------------------- -type private JsonParser(jsonText:string, cultureInfo, tolerateErrors) = +type private JsonParser(jsonText: string, cultureInfo, tolerateErrors) = - let cultureInfo = defaultArg cultureInfo CultureInfo.InvariantCulture + let cultureInfo = + defaultArg cultureInfo CultureInfo.InvariantCulture let mutable i = 0 let s = jsonText - + let buf = StringBuilder() // pre-allocate buffers for strings // Helper functions - let skipWhitespace() = - while i < s.Length && Char.IsWhiteSpace s.[i] do - i <- i + 1 - let decimalSeparator = cultureInfo.NumberFormat.NumberDecimalSeparator.[0] + let skipWhitespace () = + while i < s.Length && Char.IsWhiteSpace s.[i] do + i <- i + 1 + + let decimalSeparator = + cultureInfo.NumberFormat.NumberDecimalSeparator.[0] + let isNumChar c = - Char.IsDigit c || c=decimalSeparator || c='e' || c='E' || c='+' || c='-' - let throw() = - let msg = - sprintf - "Invalid JSON starting at character %d, snippet = \n----\n%s\n-----\njson = \n------\n%s\n-------" - i (jsonText.[(max 0 (i-10))..(min (jsonText.Length-1) (i+10))]) (if jsonText.Length > 1000 then jsonText.Substring(0, 1000) else jsonText) - failwith msg - let ensure cond = - if not cond then throw() + Char.IsDigit c + || c = decimalSeparator + || c = 'e' + || c = 'E' + || c = '+' + || c = '-' + + let throw () = + let msg = + sprintf + "Invalid JSON starting at character %d, snippet = \n----\n%s\n-----\njson = \n------\n%s\n-------" + i + (jsonText.[(max 0 (i - 10))..(min (jsonText.Length - 1) (i + 10))]) + (if jsonText.Length > 1000 then + jsonText.Substring(0, 1000) + else + jsonText) + + failwith msg + + let ensure cond = if not cond then throw () // Recursive descent parser for JSON that uses global mutable index - let rec parseValue() = - skipWhitespace() - ensure(i < s.Length) + let rec parseValue () = + skipWhitespace () + ensure (i < s.Length) + match s.[i] with - | '"' -> JsonValue.String(parseString()) - | '-' -> parseNum() - | c when Char.IsDigit(c) -> parseNum() - | '{' -> parseObject() - | '[' -> parseArray() - | 't' -> parseLiteral("true", JsonValue.Boolean true) - | 'f' -> parseLiteral("false", JsonValue.Boolean false) - | 'n' -> parseLiteral("null", JsonValue.Null) - | _ -> throw() - - and parseRootValue() = - skipWhitespace() - ensure(i < s.Length) + | '"' -> JsonValue.String(parseString ()) + | '-' -> parseNum () + | c when Char.IsDigit(c) -> parseNum () + | '{' -> parseObject () + | '[' -> parseArray () + | 't' -> parseLiteral ("true", JsonValue.Boolean true) + | 'f' -> parseLiteral ("false", JsonValue.Boolean false) + | 'n' -> parseLiteral ("null", JsonValue.Null) + | _ -> throw () + + and parseRootValue () = + skipWhitespace () + ensure (i < s.Length) + match s.[i] with - | '{' -> parseObject() - | '[' -> parseArray() - | _ -> throw() + | '{' -> parseObject () + | '[' -> parseArray () + | _ -> throw () - and parseString() = - ensure(i < s.Length && s.[i] = '"') + and parseString () = + ensure (i < s.Length && s.[i] = '"') i <- i + 1 + while i < s.Length && s.[i] <> '"' do if s.[i] = '\\' then - ensure(i+1 < s.Length) - match s.[i+1] with + ensure (i + 1 < s.Length) + + match s.[i + 1] with | 'b' -> buf.Append('\b') |> ignore | 'f' -> buf.Append('\f') |> ignore | 'n' -> buf.Append('\n') |> ignore @@ -205,157 +242,200 @@ type private JsonParser(jsonText:string, cultureInfo, tolerateErrors) = | '/' -> buf.Append('/') |> ignore | '"' -> buf.Append('"') |> ignore | 'u' -> - ensure(i+5 < s.Length) + ensure (i + 5 < s.Length) + let hexdigit d = - if d >= '0' && d <= '9' then int32 d - int32 '0' - elif d >= 'a' && d <= 'f' then int32 d - int32 'a' + 10 - elif d >= 'A' && d <= 'F' then int32 d - int32 'A' + 10 - else failwith "hexdigit" - let unicodeChar (s:string) = - if s.Length <> 4 then failwith "unicodeChar"; - char (hexdigit s.[0] * 4096 + hexdigit s.[1] * 256 + hexdigit s.[2] * 16 + hexdigit s.[3]) - let ch = unicodeChar (s.Substring(i+2, 4)) + if d >= '0' && d <= '9' then + int32 d - int32 '0' + elif d >= 'a' && d <= 'f' then + int32 d - int32 'a' + 10 + elif d >= 'A' && d <= 'F' then + int32 d - int32 'A' + 10 + else + failwith "hexdigit" + + let unicodeChar (s: string) = + if s.Length <> 4 then + failwith "unicodeChar" + + char ( + hexdigit s.[0] * 4096 + + hexdigit s.[1] * 256 + + hexdigit s.[2] * 16 + + hexdigit s.[3] + ) + + let ch = unicodeChar (s.Substring(i + 2, 4)) buf.Append(ch) |> ignore - i <- i + 4 // the \ and u will also be skipped past further below + i <- i + 4 // the \ and u will also be skipped past further below | 'U' -> - ensure(i+9 < s.Length) - let unicodeChar (s:string) = - if s.Length <> 8 then failwith "unicodeChar"; - if s.[0..1] <> "00" then failwith "unicodeChar"; - UnicodeHelper.getUnicodeSurrogatePair <| System.UInt32.Parse(s, NumberStyles.HexNumber) - let lead, trail = unicodeChar (s.Substring(i+2, 8)) + ensure (i + 9 < s.Length) + + let unicodeChar (s: string) = + if s.Length <> 8 then + failwith "unicodeChar" + + if s.[0..1] <> "00" then + failwith "unicodeChar" + + UnicodeHelper.getUnicodeSurrogatePair + <| System.UInt32.Parse(s, NumberStyles.HexNumber) + + let lead, trail = unicodeChar (s.Substring(i + 2, 8)) buf.Append(lead) |> ignore buf.Append(trail) |> ignore - i <- i + 8 // the \ and u will also be skipped past further below - | _ -> throw() - i <- i + 2 // skip past \ and next char + i <- i + 8 // the \ and u will also be skipped past further below + | _ -> throw () + + i <- i + 2 // skip past \ and next char else buf.Append(s.[i]) |> ignore i <- i + 1 - ensure(i < s.Length && s.[i] = '"') + + ensure (i < s.Length && s.[i] = '"') i <- i + 1 + let str = buf.ToString() buf.Clear() |> ignore str - and parseNum() = + and parseNum () = let start = i - while i < s.Length && isNumChar(s.[i]) do + + while i < s.Length && isNumChar (s.[i]) do i <- i + 1 + let len = i - start - let sub = s.Substring(start,len) + let sub = s.Substring(start, len) + match TextConversions.AsDecimal cultureInfo sub with | Some x -> JsonValue.Number x | _ -> - match TextConversions.AsFloat [| |] (*useNoneForMissingValues*)false cultureInfo sub with + match TextConversions.AsFloat [||] (*useNoneForMissingValues*) false cultureInfo sub with | Some x -> JsonValue.Float x - | _ -> throw() + | _ -> throw () - and parsePair() = - let key = parseString() - skipWhitespace() - ensure(i < s.Length && s.[i] = ':') + and parsePair () = + let key = parseString () + skipWhitespace () + ensure (i < s.Length && s.[i] = ':') i <- i + 1 - skipWhitespace() - key, parseValue() + skipWhitespace () + key, parseValue () - and parseEllipsis() = + and parseEllipsis () = let mutable openingBrace = false + if i < s.Length && s.[i] = '{' then openingBrace <- true i <- i + 1 - skipWhitespace() + skipWhitespace () + while i < s.Length && s.[i] = '.' do i <- i + 1 - skipWhitespace() + skipWhitespace () + if openingBrace && i < s.Length && s.[i] = '}' then i <- i + 1 - skipWhitespace() + skipWhitespace () - and parseObject() = - ensure(i < s.Length && s.[i] = '{') + and parseObject () = + ensure (i < s.Length && s.[i] = '{') i <- i + 1 - skipWhitespace() + skipWhitespace () + let pairs = ResizeArray<_>() + if i < s.Length && s.[i] = '"' then - pairs.Add(parsePair()) - skipWhitespace() + pairs.Add(parsePair ()) + skipWhitespace () + while i < s.Length && s.[i] = ',' do i <- i + 1 - skipWhitespace() + skipWhitespace () + if tolerateErrors && s.[i] = '}' then () // tolerate a trailing comma, even though is not valid json else - pairs.Add(parsePair()) - skipWhitespace() + pairs.Add(parsePair ()) + skipWhitespace () + if tolerateErrors && i < s.Length && s.[i] <> '}' then - parseEllipsis() // tolerate ... or {...} - ensure(i < s.Length && s.[i] = '}') + parseEllipsis () // tolerate ... or {...} + + ensure (i < s.Length && s.[i] = '}') i <- i + 1 JsonValue.Record(pairs.ToArray()) - and parseArray() = - ensure(i < s.Length && s.[i] = '[') + and parseArray () = + ensure (i < s.Length && s.[i] = '[') i <- i + 1 - skipWhitespace() + skipWhitespace () + let vals = ResizeArray<_>() + if i < s.Length && s.[i] <> ']' then - vals.Add(parseValue()) - skipWhitespace() + vals.Add(parseValue ()) + skipWhitespace () + while i < s.Length && s.[i] = ',' do i <- i + 1 - skipWhitespace() - vals.Add(parseValue()) - skipWhitespace() + skipWhitespace () + vals.Add(parseValue ()) + skipWhitespace () + if tolerateErrors && i < s.Length && s.[i] <> ']' then - parseEllipsis() // tolerate ... or {...} - ensure(i < s.Length && s.[i] = ']') + parseEllipsis () // tolerate ... or {...} + + ensure (i < s.Length && s.[i] = ']') i <- i + 1 JsonValue.Array(vals.ToArray()) - and parseLiteral(expected, r) = - ensure(i+expected.Length < s.Length) + and parseLiteral (expected, r) = + ensure (i + expected.Length < s.Length) + for j in 0 .. expected.Length - 1 do - ensure(s.[i+j] = expected.[j]) + ensure (s.[i + j] = expected.[j]) + i <- i + expected.Length r // Start by parsing the top-level value member x.Parse() = - let value = parseRootValue() - skipWhitespace() - if i <> s.Length then - throw() + let value = parseRootValue () + skipWhitespace () + if i <> s.Length then throw () value member x.ParseMultiple() = seq { while i <> s.Length do - yield parseRootValue() - skipWhitespace() + yield parseRootValue () + skipWhitespace () } type JsonValue with - /// Parses the specified JSON string - static member Parse(text, [] ?cultureInfo) = - JsonParser(text, cultureInfo, false).Parse() - - /// Loads JSON from the specified stream - static member Load(stream:Stream, [] ?cultureInfo) = - use reader = new StreamReader(stream) - let text = reader.ReadToEnd() - JsonParser(text, cultureInfo, false).Parse() - - /// Loads JSON from the specified reader - static member Load(reader:TextReader, [] ?cultureInfo) = - let text = reader.ReadToEnd() - JsonParser(text, cultureInfo, false).Parse() - - /// Parses the specified JSON string, tolerating invalid errors like trailing commans, and ignore content with elipsis ... or {...} - static member ParseSample(text, [] ?cultureInfo) = - JsonParser(text, cultureInfo, true).Parse() - - /// Parses the specified string into multiple JSON values - static member ParseMultiple(text, [] ?cultureInfo) = - JsonParser(text, cultureInfo, false).ParseMultiple() + /// Parses the specified JSON string + static member Parse(text, [] ?cultureInfo) = + JsonParser(text, cultureInfo, false).Parse() + + /// Loads JSON from the specified stream + static member Load(stream: Stream, [] ?cultureInfo) = + use reader = new StreamReader(stream) + let text = reader.ReadToEnd() + JsonParser(text, cultureInfo, false).Parse() + + /// Loads JSON from the specified reader + static member Load(reader: TextReader, [] ?cultureInfo) = + let text = reader.ReadToEnd() + JsonParser(text, cultureInfo, false).Parse() + + /// Parses the specified JSON string, tolerating invalid errors like trailing commans, and ignore content with elipsis ... or {...} + static member ParseSample(text, [] ?cultureInfo) = + JsonParser(text, cultureInfo, true).Parse() + + /// Parses the specified string into multiple JSON values + static member ParseMultiple(text, [] ?cultureInfo) = + JsonParser(text, cultureInfo, false) + .ParseMultiple() diff --git a/FSharp.Json/JsonValueHelpers.fs b/FSharp.Json/JsonValueHelpers.fs index 7e8ae30..851a2f8 100644 --- a/FSharp.Json/JsonValueHelpers.fs +++ b/FSharp.Json/JsonValueHelpers.fs @@ -6,7 +6,9 @@ module internal JsonValueHelpers = open Conversions let raiseWrongType path typeName jvalue = - raise(JsonDeserializationError(path, sprintf "Expected type %s is incompatible with jvalue: %A" typeName jvalue)) + raise ( + JsonDeserializationError(path, sprintf "Expected type %s is incompatible with jvalue: %A" typeName jvalue) + ) let getInt16 (path: JsonPath) (jvalue: JsonValue) = match jvalue with @@ -19,7 +21,7 @@ module internal JsonValueHelpers = | JsonValue.Number value -> uint16 value | JsonValue.Float value -> uint16 value | _ -> raiseWrongType path "uint16" jvalue - + let getInt (path: JsonPath) (jvalue: JsonValue) = match jvalue with | JsonValue.Number value -> int value @@ -31,7 +33,7 @@ module internal JsonValueHelpers = | JsonValue.Number value -> uint32 value | JsonValue.Float value -> uint32 value | _ -> raiseWrongType path "uint32" jvalue - + let getInt64 (path: JsonPath) (jvalue: JsonValue) = match jvalue with | JsonValue.Number value -> int64 value @@ -55,7 +57,7 @@ module internal JsonValueHelpers = | JsonValue.Float value -> single value | JsonValue.Number value -> single value | _ -> raiseWrongType path "single" jvalue - + let getFloat (path: JsonPath) (jvalue: JsonValue) = match jvalue with | JsonValue.Float value -> value @@ -79,7 +81,7 @@ module internal JsonValueHelpers = | JsonValue.Number value -> sbyte value | JsonValue.Float value -> sbyte value | _ -> raiseWrongType path "sbyte" jvalue - + let getBool (path: JsonPath) (jvalue: JsonValue) = match jvalue with | JsonValue.Boolean value -> value @@ -95,13 +97,21 @@ module internal JsonValueHelpers = | JsonValue.String value -> match value.Length with | 1 -> value.Chars(0) - | _ -> raise(JsonDeserializationError(path, sprintf "Expected string with single character, got jvalue: %s" value)) + | _ -> + raise ( + JsonDeserializationError( + path, + sprintf "Expected string with single character, got jvalue: %s" value + ) + ) | _ -> raiseWrongType path "char" jvalue let getDateTime cultureInfo (path: JsonPath) (jvalue: JsonValue) = match jvalue with - | JsonValue.String value -> - let jvalue = TextConversions.AsDateTime cultureInfo value + | JsonValue.String value -> + let jvalue = + TextConversions.AsDateTime cultureInfo value + match jvalue with | Some jvalue -> jvalue | None -> raiseWrongType path "DateTime" jvalue @@ -109,8 +119,9 @@ module internal JsonValueHelpers = let getDateTimeOffset cultureInfo (path: JsonPath) (jvalue: JsonValue) = match jvalue with - | JsonValue.String value -> + | JsonValue.String value -> let jvalue = AsDateTimeOffset cultureInfo value + match jvalue with | Some jvalue -> jvalue | None -> raiseWrongType path "DateTimeOffset" jvalue @@ -118,8 +129,9 @@ module internal JsonValueHelpers = let getGuid (path: JsonPath) (jvalue: JsonValue) = match jvalue with - | JsonValue.String value -> + | JsonValue.String value -> let jvalue = TextConversions.AsGuid value + match jvalue with | Some jvalue -> jvalue | None -> raiseWrongType path "Guid" jvalue diff --git a/FSharp.Json/Reflection.fs b/FSharp.Json/Reflection.fs index a62e73c..0c94db5 100644 --- a/FSharp.Json/Reflection.fs +++ b/FSharp.Json/Reflection.fs @@ -4,57 +4,86 @@ module internal Reflection = open System open System.Reflection open System.Collections.Concurrent - open Microsoft.FSharp.Reflection + open Microsoft.FSharp.Reflection - let isOption_ (t: Type): bool = - t.IsGenericType && t.GetGenericTypeDefinition() = typedefof> + let isOption_ (t: Type) : bool = + t.IsGenericType + && t.GetGenericTypeDefinition() = typedefof> - let getOptionType_ (t: Type): Type = - t.GetGenericArguments().[0] + let getOptionType_ (t: Type) : Type = t.GetGenericArguments().[0] - let isArray_ (t: Type) = - t.IsArray + let isArray_ (t: Type) = t.IsArray let isList_ (t: Type) = - t.IsGenericType && t.GetGenericTypeDefinition() = typedefof> + t.IsGenericType + && t.GetGenericTypeDefinition() = typedefof> let getListType_ (itemType: Type) = - typedefof>.MakeGenericType([| itemType |]) + typedefof>.MakeGenericType ([| itemType |]) - let getListItemType_ (t: Type) = - t.GetGenericArguments().[0] + let getListItemType_ (t: Type) = t.GetGenericArguments().[0] - let getListConstructor_ (t: Type) = - t.GetMethod ("Cons") + let getListConstructor_ (t: Type) = t.GetMethod("Cons") - let getListEmptyProperty_ (t: Type) = - t.GetProperty("Empty") + let getListEmptyProperty_ (t: Type) = t.GetProperty("Empty") + + let getIEnumerableType_ (itemType: Type) = + typedefof>.MakeGenericType ([| itemType |]) + + let isSet_ (t: Type) = + t.IsGenericType + && t.GetGenericTypeDefinition() = typedefof> + + let getSetType_ (itemType: Type) = + typedefof>.MakeGenericType ([| itemType |]) + + let getSetItemType_ (t: Type) = t.GetGenericArguments().[0] + + let getSetConstructor_ (setType: Type, enumType: Type) = setType.GetConstructor([| enumType |]) + let getSetAdd_ (t: Type) = t.GetMethod("Add") + + let isResizeArray_ (t: Type) = + t.IsGenericType + && t.GetGenericTypeDefinition() = typedefof> + + let getResizeArrayType_ (itemType: Type) = + typedefof>.MakeGenericType ([| itemType |]) + + let getResizeArrayItemType_ (t: Type) = t.GetGenericArguments().[0] + + let getResizeArrayConstructor_ (t: Type) = t.GetConstructor([||]) + + let getResizeArrayAdd_ (t: Type) = t.GetMethod("Add") let isMap_ (t: Type) = - t.IsGenericType && t.GetGenericTypeDefinition() = typedefof> + t.IsGenericType + && t.GetGenericTypeDefinition() = typedefof> - let getMapKeyType_ (t: Type) = - t.GetGenericArguments().[0] + let getMapKeyType_ (t: Type) = t.GetGenericArguments().[0] - let getMapValueType_ (t: Type) = - t.GetGenericArguments().[1] + let getMapValueType_ (t: Type) = t.GetGenericArguments().[1] let getMapKvpTupleType_ (t: Type) = - t.GetGenericArguments() |> FSharpType.MakeTupleType + t.GetGenericArguments() + |> FSharpType.MakeTupleType - let cacheResult (theFunction:'P -> 'R) = + let cacheResult (theFunction: 'P -> 'R) = let theFunction = new Func<_, _>(theFunction) let cache = new ConcurrentDictionary<'P, 'R>() fun parameter -> cache.GetOrAdd(parameter, theFunction) let isRecord: Type -> bool = FSharpType.IsRecord |> cacheResult - let getRecordFields: Type -> PropertyInfo [] = FSharpType.GetRecordFields |> cacheResult + + let getRecordFields: Type -> PropertyInfo [] = + FSharpType.GetRecordFields |> cacheResult let isUnion: Type -> bool = FSharpType.IsUnion |> cacheResult let getUnionCases: Type -> UnionCaseInfo [] = FSharpType.GetUnionCases |> cacheResult let isTuple: Type -> bool = FSharpType.IsTuple |> cacheResult - let getTupleElements: Type -> Type [] = FSharpType.GetTupleElements |> cacheResult + + let getTupleElements: Type -> Type [] = + FSharpType.GetTupleElements |> cacheResult let isOption: Type -> bool = isOption_ |> cacheResult let getOptionType: Type -> Type = getOptionType_ |> cacheResult @@ -67,43 +96,101 @@ module internal Reflection = let getListConstructor: Type -> MethodInfo = getListConstructor_ |> cacheResult let getListEmptyProperty: Type -> PropertyInfo = getListEmptyProperty_ |> cacheResult + let getIEnumerableType: Type -> Type = getIEnumerableType_ |> cacheResult + + let isSet: Type -> bool = isSet_ |> cacheResult + let getSetType: Type -> Type = getSetType_ |> cacheResult + let getSetItemType: Type -> Type = getSetItemType_ |> cacheResult + let getSetConstructor: Type * Type -> ConstructorInfo = getSetConstructor_ |> cacheResult + let getSetAdd: Type -> MethodInfo = getSetAdd_ |> cacheResult + + let isResizeArray: Type -> bool = isResizeArray_ |> cacheResult + let getResizeArrayType: Type -> Type = getResizeArrayType_ |> cacheResult + let getResizeArrayItemType: Type -> Type = getResizeArrayItemType_ |> cacheResult + let getResizeArrayAdd: Type -> MethodInfo = getResizeArrayAdd_ |> cacheResult + + let getResizeArrayConstructor: Type -> ConstructorInfo = + getResizeArrayConstructor_ |> cacheResult + let isMap: Type -> bool = isMap_ |> cacheResult let getMapKeyType: Type -> Type = getMapKeyType_ |> cacheResult let getMapValueType: Type -> Type = getMapValueType_ |> cacheResult let getMapKvpTupleType: Type -> Type = getMapKvpTupleType_ |> cacheResult - let unwrapOption (t: Type) (value: obj): obj option = + let unwrapOption (t: Type) (value: obj) : obj option = let _, fields = FSharpValue.GetUnionFields(value, t) + match fields.Length with | 1 -> Some fields.[0] | _ -> None - let optionNone (t: Type): obj = + let optionNone (t: Type) : obj = let casesInfos = getUnionCases t - FSharpValue.MakeUnion(casesInfos.[0], Array.empty) + FSharpValue.MakeUnion(casesInfos.[0], Array.empty) - let optionSome (t: Type) (value: obj): obj = + let optionSome (t: Type) (value: obj) : obj = let casesInfos = getUnionCases t FSharpValue.MakeUnion(casesInfos.[1], [| value |]) let createList (itemType: Type) (items: obj list) = let listType = getListType itemType let theConstructor = getListConstructor listType - let addItem item list = theConstructor.Invoke (null, [| item; list |]) - let theList = (getListEmptyProperty listType).GetValue(null) + + let addItem item list = + theConstructor.Invoke(null, [| item; list |]) + + let theList = + (getListEmptyProperty listType).GetValue(null) + List.foldBack addItem items theList - let KvpKey (value: obj): obj = + let createSet (itemType: Type) (items: obj list) = + let setType = getSetType itemType + let enumType = getIEnumerableType itemType + let setConstructor = getSetConstructor (setType, enumType) + + let listEmpty = + getListType itemType |> getListEmptyProperty + + let setAdd item set = + setType.GetMethod("Add").Invoke(set, [| item |]) + + let newSet = + setConstructor.Invoke([| listEmpty.GetValue(null) |]) + + List.foldBack setAdd items newSet + + let createResizeArray (itemType: Type) (items: obj list) = + let resizeArrayType = getResizeArrayType itemType + + let resizeArrayAdd resizeArray item = + (getResizeArrayAdd resizeArrayType) + .Invoke(resizeArray, [| item |]) + |> ignore + + resizeArray + + let newResizeArray = + (getResizeArrayConstructor resizeArrayType) + .Invoke([||]) + + List.fold resizeArrayAdd newResizeArray items + + let KvpKey (value: obj) : obj = let keyProperty = value.GetType().GetProperty("Key") keyProperty.GetValue(value, null) - let KvpValue (value: obj): obj = + let KvpValue (value: obj) : obj = let valueProperty = value.GetType().GetProperty("Value") valueProperty.GetValue(value, null) - let CreateMap (mapType: Type) (items: (string*obj) list) = + let CreateMap (mapType: Type) (items: (string * obj) list) = let theConstructor = mapType.GetConstructors().[0] let tupleType = getMapKvpTupleType mapType - let listItems = items |> List.map (fun item -> FSharpValue.MakeTuple([|fst item; snd item|], tupleType)) + + let listItems = + items + |> List.map (fun item -> FSharpValue.MakeTuple([| fst item; snd item |], tupleType)) + let theList = createList tupleType listItems - theConstructor.Invoke([|theList|]) + theConstructor.Invoke([| theList |]) diff --git a/FSharp.Json/TextConversions.fs b/FSharp.Json/TextConversions.fs index 3d8477b..9c611f9 100644 --- a/FSharp.Json/TextConversions.fs +++ b/FSharp.Json/TextConversions.fs @@ -1,123 +1,198 @@ // -------------------------------------------------------------------------------------- // Helper operations for converting converting string values to other types // -------------------------------------------------------------------------------------- - namespace FSharp.Json.Internalized.FSharp.Data open System open System.Globalization open System.Text.RegularExpressions -// -------------------------------------------------------------------------------------- - [] module private Helpers = - - /// Convert the result of TryParse to option type - let asOption = function true, v -> Some v | _ -> None - - let (|StringEqualsIgnoreCase|_|) (s1:string) s2 = - if s1.Equals(s2, StringComparison.OrdinalIgnoreCase) - then Some () else None - - let (|OneOfIgnoreCase|_|) set str = - if Array.exists (fun s -> StringComparer.OrdinalIgnoreCase.Compare(s, str) = 0) set then Some() else None - - let regexOptions = + /// Convert the result of TryParse to option type + let asOption = + function + | true, v -> Some v + | _ -> None + + let (|StringEqualsIgnoreCase|_|) (s1: string) s2 = + if s1.Equals(s2, StringComparison.OrdinalIgnoreCase) then + Some() + else + None + + let (|OneOfIgnoreCase|_|) set str = + if Array.exists (fun s -> StringComparer.OrdinalIgnoreCase.Compare(s, str) = 0) set then + Some() + else + None + + let regexOptions = #if FX_NO_REGEX_COMPILATION - RegexOptions.None + RegexOptions.None #else - RegexOptions.Compiled + RegexOptions.Compiled #endif - // note on the regex we have /Date()/ and not \/Date()\/ because the \/ escaping - // is already taken care of before AsDateTime is called - let msDateRegex = lazy Regex(@"^/Date\((-?\d+)(?:[-+]\d+)?\)/$", regexOptions) - -// -------------------------------------------------------------------------------------- + // note on the regex we have /Date()/ and not \/Date()\/ because the \/ escaping + // is already taken care of before AsDateTime is called + let msDateRegex = + lazy (Regex(@"^/Date\((-?\d+)(?:[-+]\d+)?\)/$", regexOptions)) /// Conversions from string to string/int/int64/decimal/float/boolean/datetime/guid options -type internal TextConversions private() = - - /// `NaN` `NA` `N/A` `#N/A` `:` `-` `TBA` `TBD` - static member val DefaultMissingValues = [| "NaN"; "NA"; "N/A"; "#N/A"; ":"; "-"; "TBA"; "TBD" |] - - /// `%` `‰` `‱` - static member val DefaultNonCurrencyAdorners = [| '%'; '‰'; '‱' |] |> Set.ofArray - - /// `¤` `$` `¢` `£` `¥` `₱` `﷼` `₤` `₭` `₦` `₨` `₩` `₮` `€` `฿` `₡` `៛` `؋` `₴` `₪` `₫` `₹` `ƒ` - static member val DefaultCurrencyAdorners = [| '¤'; '$'; '¢'; '£'; '¥'; '₱'; '﷼'; '₤'; '₭'; '₦'; '₨'; '₩'; '₮'; '€'; '฿'; '₡'; '៛'; '؋'; '₴'; '₪'; '₫'; '₹'; 'ƒ' |] |> Set.ofArray - - static member val private DefaultRemovableAdornerCharacters = - Set.union TextConversions.DefaultNonCurrencyAdorners TextConversions.DefaultCurrencyAdorners - - //This removes any adorners that might otherwise casue the inference to infer string. A notable a change is - //Currency Symbols are now treated as an Adorner like a '%' sign thus are now independant - //of the culture. Which is probably better since we have lots of scenarios where we want to - //consume values prefixed with € or $ but in a different culture. - static member private RemoveAdorners (value:string) = - String(value.ToCharArray() |> Array.filter (not << TextConversions.DefaultRemovableAdornerCharacters.Contains)) - - /// Turns empty or null string value into None, otherwise returns Some - static member AsString str = - if String.IsNullOrWhiteSpace str then None else Some str - - static member AsInteger cultureInfo text = - Int32.TryParse(TextConversions.RemoveAdorners text, NumberStyles.Integer, cultureInfo) |> asOption - - static member AsInteger64 cultureInfo text = - Int64.TryParse(TextConversions.RemoveAdorners text, NumberStyles.Integer, cultureInfo) |> asOption - - static member AsDecimal cultureInfo text = - Decimal.TryParse(TextConversions.RemoveAdorners text, NumberStyles.Currency, cultureInfo) |> asOption - - /// if useNoneForMissingValues is true, NAs are returned as None, otherwise Some Double.NaN is used - static member AsFloat missingValues useNoneForMissingValues cultureInfo (text:string) = - match text.Trim() with - | OneOfIgnoreCase missingValues -> if useNoneForMissingValues then None else Some Double.NaN - | _ -> Double.TryParse(text, NumberStyles.Any, cultureInfo) - |> asOption - |> Option.bind (fun f -> if useNoneForMissingValues && Double.IsNaN f then None else Some f) - - static member AsBoolean (text:string) = - match text.Trim() with - | StringEqualsIgnoreCase "true" | StringEqualsIgnoreCase "yes" | StringEqualsIgnoreCase "1" -> Some true - | StringEqualsIgnoreCase "false" | StringEqualsIgnoreCase "no" | StringEqualsIgnoreCase "0" -> Some false - | _ -> None - - /// Parse date time using either the JSON milliseconds format or using ISO 8601 - /// that is, either `/Date()/` or something - /// along the lines of `2013-01-28T00:37Z` - static member AsDateTime cultureInfo (text:string) = - // Try parse "Date()" style format - let matchesMS = msDateRegex.Value.Match (text.Trim()) - if matchesMS.Success then - matchesMS.Groups.[1].Value - |> Double.Parse - |> DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds - |> Some - else - // Parse ISO 8601 format, fixing time zone if needed - let dateTimeStyles = DateTimeStyles.AllowWhiteSpaces ||| DateTimeStyles.RoundtripKind - match DateTime.TryParse(text, cultureInfo, dateTimeStyles) with - | true, d -> - if d.Kind = DateTimeKind.Unspecified then - new DateTime(d.Ticks, DateTimeKind.Local) |> Some - else - Some d - | _ -> None - - static member AsGuid (text:string) = - Guid.TryParse(text.Trim()) |> asOption +type internal TextConversions private () = + /// `NaN` `NA` `N/A` `#N/A` `:` `-` `TBA` `TBD` + static member val DefaultMissingValues = + [| "NaN" + "NA" + "N/A" + "#N/A" + ":" + "-" + "TBA" + "TBD" |] + + /// `%` `‰` `‱` + static member val DefaultNonCurrencyAdorners = [| '%'; '‰'; '‱' |] |> Set.ofArray + + /// `¤` `$` `¢` `£` `¥` `₱` `﷼` `₤` `₭` `₦` `₨` `₩` `₮` `€` `฿` `₡` `៛` `؋` `₴` `₪` `₫` `₹` `ƒ` + static member val DefaultCurrencyAdorners = + [| '¤' + '$' + '¢' + '£' + '¥' + '₱' + '﷼' + '₤' + '₭' + '₦' + '₨' + '₩' + '₮' + '€' + '฿' + '₡' + '៛' + '؋' + '₴' + '₪' + '₫' + '₹' + 'ƒ' |] + |> Set.ofArray + + static member val private DefaultRemovableAdornerCharacters = + Set.union TextConversions.DefaultNonCurrencyAdorners TextConversions.DefaultCurrencyAdorners + + //This removes any adorners that might otherwise casue the inference to infer string. A notable a change is + //Currency Symbols are now treated as an Adorner like a '%' sign thus are now independant + //of the culture. Which is probably better since we have lots of scenarios where we want to + //consume values prefixed with € or $ but in a different culture. + static member private RemoveAdorners(value: string) = + String( + value.ToCharArray() + |> Array.filter ( + not + << TextConversions.DefaultRemovableAdornerCharacters.Contains + ) + ) + + /// Turns empty or null string value into None, otherwise returns Some + static member AsString str = + if String.IsNullOrWhiteSpace str then + None + else + Some str + + static member AsInteger cultureInfo text = + Int32.TryParse(TextConversions.RemoveAdorners text, NumberStyles.Integer, cultureInfo) + |> asOption + + static member AsInteger64 cultureInfo text = + Int64.TryParse(TextConversions.RemoveAdorners text, NumberStyles.Integer, cultureInfo) + |> asOption + + static member AsDecimal cultureInfo text = + Decimal.TryParse(TextConversions.RemoveAdorners text, NumberStyles.Currency, cultureInfo) + |> asOption + + /// if useNoneForMissingValues is true, NAs are returned as None, otherwise Some Double.NaN is used + static member AsFloat missingValues useNoneForMissingValues cultureInfo (text: string) = + match text.Trim() with + | OneOfIgnoreCase missingValues -> + if useNoneForMissingValues then + None + else + Some Double.NaN + | _ -> + Double.TryParse(text, NumberStyles.Any, cultureInfo) + |> asOption + |> Option.bind (fun f -> + if useNoneForMissingValues && Double.IsNaN f then + None + else + Some f) + + static member AsBoolean(text: string) = + match text.Trim() with + | StringEqualsIgnoreCase "true" + | StringEqualsIgnoreCase "yes" + | StringEqualsIgnoreCase "1" -> Some true + | StringEqualsIgnoreCase "false" + | StringEqualsIgnoreCase "no" + | StringEqualsIgnoreCase "0" -> Some false + | _ -> None + + /// Parse date time using either the JSON milliseconds format or using ISO 8601 + /// that is, either `/Date()/` or something + /// along the lines of `2013-01-28T00:37Z` + static member AsDateTime cultureInfo (text: string) = + // Try parse "Date()" style format + let matchesMS = msDateRegex.Value.Match(text.Trim()) + + if matchesMS.Success then + matchesMS.Groups.[1].Value + |> Double.Parse + |> DateTime( + 1970, + 1, + 1, + 0, + 0, + 0, + DateTimeKind.Utc + ) + .AddMilliseconds + |> Some + else + // Parse ISO 8601 format, fixing time zone if needed + let dateTimeStyles = + DateTimeStyles.AllowWhiteSpaces + ||| DateTimeStyles.RoundtripKind + + match DateTime.TryParse(text, cultureInfo, dateTimeStyles) with + | true, d -> + if d.Kind = DateTimeKind.Unspecified then + new DateTime(d.Ticks, DateTimeKind.Local) |> Some + else + Some d + | _ -> None + + static member AsGuid(text: string) = Guid.TryParse(text.Trim()) |> asOption module internal UnicodeHelper = - // used http://en.wikipedia.org/wiki/UTF-16#Code_points_U.2B010000_to_U.2B10FFFF as a guide below let getUnicodeSurrogatePair num = // only code points U+010000 to U+10FFFF supported // for coversion to UTF16 surrogate pair let codePoint = num - 0x010000u - let HIGH_TEN_BIT_MASK = 0xFFC00u // 1111|1111|1100|0000|0000 - let LOW_TEN_BIT_MASK = 0x003FFu // 0000|0000|0011|1111|1111 - let leadSurrogate = (codePoint &&& HIGH_TEN_BIT_MASK >>> 10) + 0xD800u - let trailSurrogate = (codePoint &&& LOW_TEN_BIT_MASK) + 0xDC00u + let HIGH_TEN_BIT_MASK = 0xFFC00u // 1111|1111|1100|0000|0000 + let LOW_TEN_BIT_MASK = 0x003FFu // 0000|0000|0011|1111|1111 + + let leadSurrogate = + (codePoint &&& HIGH_TEN_BIT_MASK >>> 10) + 0xD800u + + let trailSurrogate = + (codePoint &&& LOW_TEN_BIT_MASK) + 0xDC00u + char leadSurrogate, char trailSurrogate diff --git a/FSharp.Json/Transforms.fs b/FSharp.Json/Transforms.fs index b598354..82fbed2 100644 --- a/FSharp.Json/Transforms.fs +++ b/FSharp.Json/Transforms.fs @@ -7,20 +7,53 @@ module Transforms = /// Implementation of [ITypeTransform] converting DateTime into int64 as epoch time. type DateTimeEpoch() = interface ITypeTransform with - member x.targetType () = (fun _ -> typeof) () - member x.toTargetType value = (fun (v: obj) -> int64(((v :?> DateTime) - DateTime(1970, 1, 1)).TotalSeconds) :> obj) value - member x.fromTargetType value = (fun (v: obj) -> DateTime(1970, 1, 1).Add(TimeSpan.FromSeconds(float(v :?> int64))) :> obj) value + member x.targetType() = (fun _ -> typeof) () + + member x.toTargetType value = + (fun (v: obj) -> + int64 ( + ((v :?> DateTime) - DateTime(1970, 1, 1)) + .TotalSeconds + ) + :> obj) + value + + member x.fromTargetType value = + (fun (v: obj) -> + DateTime(1970, 1, 1) + .Add(TimeSpan.FromSeconds(float (v :?> int64))) + :> obj) + value /// Implementation of [ITypeTransform] converting DateTimeOffset into int64 as epoch time. type DateTimeOffsetEpoch() = interface ITypeTransform with - member x.targetType () = (fun _ -> typeof) () - member x.toTargetType value = (fun (v: obj) -> int64(((v :?> DateTimeOffset) - DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan(0L))).TotalSeconds) :> obj) value - member x.fromTargetType value = (fun (v: obj) -> DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan(0L)).Add(TimeSpan.FromSeconds(float(v :?> int64))) :> obj) value + member x.targetType() = (fun _ -> typeof) () + + member x.toTargetType value = + (fun (v: obj) -> + int64 ( + ((v :?> DateTimeOffset) + - DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan(0L))) + .TotalSeconds + ) + :> obj) + value + + member x.fromTargetType value = + (fun (v: obj) -> + DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan(0L)) + .Add(TimeSpan.FromSeconds(float (v :?> int64))) + :> obj) + value /// Implementation of [ITypeTransform] converting Uri into string. type UriTransform() = interface ITypeTransform with - member x.targetType () = (fun _ -> typeof) () - member x.toTargetType value = (fun (v: obj) -> (v:?> System.Uri).ToString() :> obj) value - member x.fromTargetType value = (fun (v: obj) -> Uri(v :?> string) :> obj) value \ No newline at end of file + member x.targetType() = (fun _ -> typeof) () + + member x.toTargetType value = + (fun (v: obj) -> (v :?> System.Uri).ToString() :> obj) value + + member x.fromTargetType value = + (fun (v: obj) -> Uri(v :?> string) :> obj) value diff --git a/FSharp.Json/Utils.fs b/FSharp.Json/Utils.fs index 4b3a877..da59dba 100644 --- a/FSharp.Json/Utils.fs +++ b/FSharp.Json/Utils.fs @@ -4,9 +4,13 @@ module internal Conversions = open System open System.Globalization - let AsDateTimeOffset cultureInfo (text: string) = - // Parse ISO 8601 format, fixing time zone if needed - let dateTimeStyles = DateTimeStyles.AllowWhiteSpaces ||| DateTimeStyles.RoundtripKind ||| DateTimeStyles.AssumeUniversal - match DateTimeOffset.TryParse(text, cultureInfo, dateTimeStyles) with - | true, d -> Some d - | _ -> None + let AsDateTimeOffset cultureInfo (text: string) = + // Parse ISO 8601 format, fixing time zone if needed + let dateTimeStyles = + DateTimeStyles.AllowWhiteSpaces + ||| DateTimeStyles.RoundtripKind + ||| DateTimeStyles.AssumeUniversal + + match DateTimeOffset.TryParse(text, cultureInfo, dateTimeStyles) with + | true, d -> Some d + | _ -> None diff --git a/FSharp.Json/paket.references b/FSharp.Json/paket.references new file mode 100644 index 0000000..6f627f4 --- /dev/null +++ b/FSharp.Json/paket.references @@ -0,0 +1 @@ +FSharp.Core diff --git a/README.md b/README.md index 6b51138..a8e4152 100644 --- a/README.md +++ b/README.md @@ -1,738 +1,743 @@ -# FSharp.Json: JSON Serialization Library - -FSharp.Json is F# JSON serialization library based on Reflection it's written in F# for F#. - -## Basic Usage Example - -Here's basic example of FSharp.Json usage: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -// Your record type -type RecordType = { - stringMember: string - intMember: int -} - -let data: RecordType = { stringMember = "The string"; intMember = 123 } - -// serialize record into JSON -let json = Json.serialize data -printfn "%s" json -// json is """{ "stringMember": "The string", "intMember": 123 }""" - -// deserialize from JSON to record -let deserialized = Json.deserialize json -printfn "%A" deserialized -// deserialized is {stringMember = "some value"; intMember = 123;} -``` - -## Table of Contents - - [Basic Usage Example](#basic-usage-example) - - [Table of Contents](#table-of-contents) - - [Why?](#why) - - [How?](#how) - - [Documentation](#documentation) - - [API Overview](#api-overview) - - [Advanced functions](#advanced-functions) - - [Configuration](#configuration) - - [Unformatted JSON](#unformatted-json) - - [Supported Types](#supported-types) - - [Customizing JSON Fields Names](#customizing-json-fields-names) - - [Change JSON field name](#change-json-field-name) - - [Change all fields names](#change-all-fields-names) - - [Null Safety](#null-safety) - - [Deserialization of null](#deserialization-of-null) - - [Customization of null deserialization](#customization-of-null-deserialization) - - [Serialization of None](#serialization-of-none) - - [Customization of None serialization](#customization-of-none-serialization) - - [Enums](#enums) - - [Customizing enum serialization](#customizing-enum-serialization) - - [Default enum behaviour](#default-enum-behaviour) - - [Unions](#unions) - - [Changing union case key](#changing-union-case-key) - - [Union case without fields](#union-case-without-fields) - - [Single case union](#single-case-union) - - [Union modes](#union-modes) - - [Type Transform](#type-transform) - - [DateTime as epoch time](#datetime-as-epoch-time) - - [System.Uri as string](#systemuri-as-string) - - [Transform by default](#transform-by-default) - - [Untyped Data](#untyped-data) - - [Serialization of obj](#serialization-of-obj) - - [Deserialization of obj](#deserialization-of-obj) - - [Release Notes](#release-notes) - - [Contributing and copyright](#contributing-and-copyright) - - [Maintainer(s)](#maintainers) - -## Why? - -Why we need yet another F# JSON serialization library? -Well, if you happy with the library that you are using currently then probably you do not need another one. -There are several available options to choose from: - - * [JSON Type Provider](http://fsharp.github.io/FSharp.Data/library/JsonProvider.html) - * [Json.Net aka Newtonsoft.Json](https://www.newtonsoft.com/json) - * [Chiron](https://github.com/xyncro/chiron) - * [JsonFSharp](https://github.com/PeteProgrammer/JsonFSharp) - * [Thoth.Json](https://mangelmaxime.github.io/Thoth/json/v2.html#code-sample) - * [FSharpLu.Json](https://github.com/Microsoft/fsharplu/tree/master/FSharpLu.Json) - * Let me know what library I missed here - -While all of these libraries do provide some good functionality all of them seem to have a weakness. -Some of them are written in C# without out of the box support for F# types. -Additionally null safety is alien concept in C# however it is a must in F#. -Other libraries provide only low-level functionality leaving a lot of cumbersome coding work to library user. -The type provider library forces developer to define JSON schema in form of json examples which is far from convenient way of defining schemas. - -FSharp.Json was developed with these values in mind: - - * Easy to use API - * Automatic mapping of F# types into JSON - * Out of the box support for F# types - * Null safety - -## How? - -FSharp.Json is pure F# library. -It uses [Reflection][reflection] to get information about F# types at runtime. -The code from [FSharp.Data library][fsharp_data] is used for parsing JSON. -The [JsonValue type][jsonvalue_type] is internalized in the FSharp.Json library. -The core of FSharp.Json library is located in single [Core.fs file][core]. - - [reflection]: https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/reflection - [fsharp_data]: http://fsharp.github.io/FSharp.Data/ - [jsonvalue_type]: http://fsharp.github.io/FSharp.Data/reference/fsharp-data-jsonvalue.html - [core]: https://github.com/vsapronov/FSharp.Json/blob/master/src/FSharp.Json/Core.fs - -## Documentation - -This document describe all details of FSharp.Json library. The source code also has thorough documentation in comments to main types. Each feature of FSharp.Json is thoroughly covered by [unit tests](tests/FSharp.Json.Tests). - -## API Overview - -Most of API functions are defined in [Json module](src/FSharp.Json/Interface.fs). - -Easiest way to serialize is to call `Json.serialize` function. -It serializes any supported F# type to string containing JSON. - -Similarly easiest way to deserialize is to call `Json.deserialize<'T>` function. -It takes string containing JSON and returns instance of type 'T. - -#### Advanced functions - -Functions `Json.serialize` and `Json.deserialize` are using default configuration. -Whenever custom configuration should be used following functions are useful: - - * `Json.serializeEx` - * `Json.deserializeEx<'T>` - -Prefix `Ex` stands for "extended". Both of these functions take [JsonConfig](src/FSharp.Json/InterfaceTypes.fs) instance as a first parameter. - -#### Configuration - -[JsonConfig](src/FSharp.Json/InterfaceTypes.fs) represents global configuration of serialization. -There's convenient way to override default configuration by using `JsonConfig.create` function. -All parameters of the function are optional and those that are provided override default values. - -For example, custom `jsonFieldNaming` could be found [here](#change-all-fields-names). - -#### Unformatted JSON - -Some products like [Apache Spark](https://spark.apache.org/) require unformatted JSON in a single line. -It is usefull to produce unformatted single line JSON in some other scenarios. -There is a function to produce unformatted JSON: `Json.serializeU`. -`U` stands for "unformatted". It has the same signature as `Json.serialize` function. -The function is a shorthand to using `unformatted` member on [JsonConfig](src/FSharp.Json/InterfaceTypes.fs). - -## Supported Types - -Here's full list of F# types that are supported by FSharp.Json library. - -| F# Type | JSON Type | -|:---|:---| -sbyte
byte
int16
uint16
int
uint
int64
uint64
bigint
single
float
decimal | number -string | string -char | string -bool | bool -DateTime | string according to [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)
number epoch time using [transform](#transform) -DateTimeOffset | string according to [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)
number epoch time using [transform](#transform) -Guid | string -Uri| string using [transform](#transform) -Enum | string enum value name
number enum value
read [Enums](#enums) section -option | option is not represented by itself
`None` value might be represented as `null` or omitted
read more in [Null Safety](#null-safety) section -tuple | list -record | object -map | object -array
list | list -union | object with special structure
read more in [Unions](#unions) section -obj | read [Untyped Data](#untyped-data) section - -## Customizing JSON Fields Names - -When record type instance is serialized into JSON members names are used as JSON fields names. -In some scenarios the JSON should have different fields names then F# record type. -This section describes how FSharp.Json library provides JSON customization abilities. - -#### Change JSON field name - -JSON field name could be easily customized with JsonField attribute: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -// attribute stringMember with JsonField -type RecordType = { - [] - stringMember: string - intMember: int -} - -let data: RecordType = { stringMember = "The string"; intMember = 123 } - -let json = Json.serialize data -printfn "%s" json -// json is """{ "different_name": "The string", "intMember": 123 }""" -// pay attention that "different_name" was used as JSON field name - -let deserialized = Json.deserialize json -printfn "%A" deserialized -// deserialized is {stringMember = "some value"; intMember = 123;} -``` - -In example above JsonField attribute affects both serialization and deserialization. - -#### Change all fields names - -What if all fields names should be different in JSON compared to F# member names? -This could be needed for example if naming convention used in F# does not match JSON naming convention. -FSharp.Json allows to map fields names with naming function. - -In the example below all camel case F# record members are mapped into snake case JSON fields: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -// attribute stringMember with JsonField -type RecordType = { - stringMember: string - intMember: int -} - -let data: RecordType = { stringMember = "The string"; intMember = 123 } - -// create config with JSON field naming setting -let config = JsonConfig.create(jsonFieldNaming = Json.snakeCase) - -let json = Json.serializeEx config data -printfn "%s" json -// json is """{ "string_member": "The string", "int_member": 123 }""" -// pay attention that "string_member" and "int_member" are in snake case - -let deserialized = Json.deserializeEx config json -printfn "%A" deserialized -// deserialized is {stringMember = "some value"; intMember = 123;} -``` - -The `Json` module defines two naming functions that could be used out of the box: snakeCase and lowerCamelCase. -The one can define it's own naming rule - it's just a function `string -> string`. - -## Null Safety - -FSharp.Json is null safe. -This means that library will never deserialize JSON into anything with null value. -FSharp.Json treats option types as an instruction that value could be null in JSON. -For example member of type `string` can't get null value in FSharp.Json, however `string option` member can have `None` value which is translated into null in JSON. -Same logic applies to all types supported by FSharp.Json library. -See examples in sections below. - -#### Deserialization of null - -In the example below `stringMember` member can't be assigned to null even though F# allows string to be null: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -type RecordType = { - stringMember: string -} - -let json = """{"stringMember":null}""" - -// this attempt to deserialize will throw exception -let deserialized = Json.deserialize json -``` - -What if the member `stringMember` could be null in JSON? -In such case the record type should explictly use string option type: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -type RecordType = { - stringMember: string option -} - -let json = """{"stringMember":null}""" - -// this attempt to deserialize will work fine -let deserialized = Json.deserialize json - -// deserialized.stringMember equals to None -printfn "%A" deserialized -``` - -If value could be null or missing in JSON then F# type used for corresponding member should be option. - -#### Customization of null deserialization - -What is the difference between missing field in JSON and field assigned to null? -By default FSharp.Json library treats both cases as None, however you can customize this logic. -Behaviour that is used to treat option types could be configured: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -type RecordType = { - stringMember: string option -} - -// this will work fine by default even when option field is missing -let deserialized1 = Json.deserialize "{}" - -printfn "%A" deserialized1 -// deserialized1.stringMember equals to None - -// create config that require option None to be represented as null -let config = JsonConfig.create(deserializeOption = DeserializeOption.RequireNull) - -// this will throw exception since config in use requires null for None deserialization. -let deserialized2 = Json.deserializeEx config "{}" -``` - -#### Serialization of None -If some member of `option` type has None value it will be serialized into JSON null by default: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -type RecordType = { - stringMember: string option -} - -// stringMember has None value -let data = { RecordType.stringMember = None } -let json = Json.serialize data -printfn "%s" json -// json is: """{ "stringMember": null }""" -``` - -#### Customization of None serialization -The one can control how None values are respresented in JSON through config. -Example belows shows how to omit members with None values: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -type RecordType = { - stringMember: string option -} - -// stringMember has None value -let data = { RecordType.stringMember = None } - -// create config that omits null values -let config = JsonConfig.create(serializeNone = SerializeNone.Omit) - -let json = Json.serializeEx config data -printfn "%s" json -// json is: """{}""" -``` - -## Enums - -By default enum value is represented as `string` that is enum member name. - -Check example code below: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -type NumberEnum = -| One = 1 -| Two = 2 -| Three = 3 - -// value will be represented as enum value name in JSON -type TheNumberEnum = { - value: NumberEnum -} - -let data = { TheNumberEnum.value = NumberEnum.Three } - -let json = Json.serialize data -// json is """{"value":"Three"}""" - -let deserialized = Json.deserialize json -// data is { TheNumberEnum.value = NumberEnum.Three } -``` - -#### Customizing enum serialization - -EnumValue member of [JsonField](src/FSharp.Json/InterfaceTypes.fs) attribute could be used to change serialization of enums. -There are two [modes](src/FSharp.Json/InterfaceTypes.fs) supported currently: enum value name and enum value. - -Here's an example of custom enum serialization: -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -type NumberEnum = -| One = 1 -| Two = 2 -| Three = 3 - -// value will be represented as enum value in JSON -type TheAttributedNumberEnum = { - [] - value: NumberEnum -} - -let data = { TheNumberEnum.value = NumberEnum.Three } - -let json = Json.serialize data -// json is """{"value":3}""" - -let deserialized = Json.deserialize json -// data is { TheNumberEnum.value = NumberEnum.Three } -``` - -#### Default enum behaviour - -Sometimes it's needed always serialize enum value as it's value. -Annotating each member of any enum type would be cumbersome. -[JsonConfig]() allows to override default enum behaviour. - -Check the example below: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -type NumberEnum = -| One = 1 -| Two = 2 -| Three = 3 - -// value will be represented as enum value name in JSON -type TheNumberEnum = { - value: NumberEnum -} - -let data = { TheNumberEnum.value = NumberEnum.Three } - -// create configuration instructing to serialize enum as enum value -let config = JsonConfig.create(enumValue = EnumMode.Value) - -let json = Json.serializeEx config data -// json is """{"value":3}""" -// value was serialized as enum value which is 3 - -let deserialized = Json.deserializeEx config json -// data is { TheNumberEnum.value = NumberEnum.Three } -``` - -## Unions - -JSON format does not support any data structure similiar to [F# discriminated unions](https://fsharpforfunandprofit.com/posts/discriminated-unions/). -Though it is still possible to represent union in JSON in some reasonable way. -By deafault FSharp.Json serializes union into JSON object with the only one field. -Name of the field corresponds to the union case. Value of the field corresponds to the union case value. - -Here's some example of default union serialization: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -type TheUnion = -| OneFieldCase of string -| ManyFieldsCase of string*int - -let data = OneFieldCase "The string" - -let json = Json.serialize data -// json is """{"OneFieldCase":"The string"}""" - -let deserialized = Json.deserialize json -// deserialized is OneFieldCase("The string") -``` - -#### Changing union case key - -The string that represents union case key could be changed with [JsonUnionCase attribute](src/FSharp.Json/InterfaceTypes.fs). - -See the example below: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -// OneFieldCase is attributed to be "case1" in JSON -type TheUnion = -| [] OneFieldCase of string -| ManyFieldsCase of string*int - -let data = OneFieldCase "The string" - -let json = Json.serialize data -// json is """{"case1":"The string"}""" - -let deserialized = Json.deserialize json -// deserialized is OneFieldCase("The string") -``` - -#### Single case union - -Single case union is a special scenario. -Read [here](https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/) about single case union usage. -In such case serializing union as JSON object is overkill. -It's more convenient to represent single case union the same way as a wrapped type. - -Here's example of single case union serialization: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -// Single case union type -type TheUnion = SingleCase of string - -type TheRecord = { - // value will be just a string - wrapped into union type - value: TheUnion -} - -let data = { TheRecord.value = SingleCase "The string" } - -let json = Json.serialize data -// json is """{"value":"The string"}""" - -let deserialized = Json.deserialize json -// deserialized is { TheRecord.value = SingleCase "The string" } -``` - -#### Union case without fields - -When union case does not have fields then the union value is represented by string value of the case name itself. - -Here's example of serialization union case without fields: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -// Case NoFieldCase does not have any fields -type TheUnion = -| NoFieldCase -| SingleCase of string - -type TheRecord = { - // value will be a string represting NoFieldCase - value: TheUnion -} - -let data = { TheRecord.value = NoFieldCase } - -let json = Json.serialize data -// json is """{"value":"NoFieldCase"}""" - -let deserialized = Json.deserialize json -// deserialized is { TheRecord.value = NoFieldCase } -``` - -#### Union modes - -There's [union mode](src/FSharp.Json/InterfaceTypes.fs) that represents union as JSON object with two fields. -One field is for case key and another one is for case value. This mode is called "case key as a field value" -If this mode is used then names of these two field should be provided through [JsonUnion attribute](src/FSharp.Json/InterfaceTypes.fs). - -See the example below: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -// The union will be represented as JSON object with two fields: casekey and casevalue. -[] -type TheUnion = -| OneFieldCase of string -| ManyFieldsCase of string*int - -let data = OneFieldCase "The string" - -let json = Json.serialize data -// json is """{"casekey":"OneFieldCase", "casevalue":"The string"}""" - -let deserialized = Json.deserialize json -// deserialized is OneFieldCase("The string") -``` - -## Type Transform - -[Supported types](supported-types) section maps F# types into JSON types. -What if some data needed to be represented as a different type then the default JSON type? -If changing type of the member in F# is not an option then type transform can help. - -Any data member is translated F# Type -> JSON type by [default](supported-types) types mapping. -[Type Transform](src/FSharp.Json/InterfaceTypes.fs) is applied in the middle of this translation: F# Type -> Alternative F# Type -> JSON type. -Alternative F# Type -> JSON type is still done by default types mapping, type transform is responsible for F# Type -> Alternative F# Type. - -The [Transforms](src/FSharp.Json/Transforms.fs) module contains transforms that are defined by FSharp.Json library. -You can define your own transforms by implementing [ITypeTransform interface](src/FSharp.Json/InterfaceTypes.fs). - -#### DateTime as epoch time - -Let's imagine that some DateTime member should be represented as [epoch time](https://en.wikipedia.org/wiki/Unix_time) in JSON. -Epoch time is int64 however it is still convenient to work with DateTime in F# code. -In such case [DateTimeEpoch transform](src/FSharp.Json/Transforms.fs) is useful. - -Here's an example of DateTimeEpoch transform usage: - -```fsharp -#r "FSharp.Json.dll" -open System -open FSharp.Json - -// value will be represented as epoch time in JSON -type DateTimeRecord = { - [)>] - value: DateTime -} - -let json = Json.serialize { DateTimeRecord.value = new DateTime(2017, 11, 5, 22, 50, 45) } -// json is """{"value":1509922245}""" - -let deserialized = Json.deserialize json -// deserialized is { DateTimeRecord.value = new DateTime(2017, 11, 5, 22, 50, 45) } -``` - -#### System.Uri as string - -This transformer transforms a Uri to/from a string for serialization. On deserialization, the string value is -handed to the System.Uri constructor. When the URI string is malformed, a UriFormatException might be thrown. - -Example use: - -```fsharp -#r "FSharp.Json.dll" -open System -open FSharp.Json - -// value will be represented as epoch time in JSON -type UriRecord = { - [)>] - value: Uri -} - -let json = Json.serialize { UriRecord.value = Uri("http://localhost:8080/") } -// json is """{"value":"http://localhost:8080/"}""" - -let deserialized = Json.deserialize json -// deserialized is { UriRecord.value = Uri("http://localhost:8080/") } -``` - -#### Transform by default - -To be developed.... - -## Untyped Data - -Using obj type in F# code is bad code smell. -Though FSharp.Json can serialize and deserialize structures without type information. -For allowing obj type in serialization/deserialization allowUntyped flag should be set to `true` on [JsonConfig](src/FSharp.Json/InterfaceTypes.fs). - -#### Serialization of obj - -When serializing obj FSharp.Json uses real run time type. - -Check this example: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -// Record type with obj member -type ObjectRecord = { - value: obj -} - -// Set string value to obj member -let data = { ObjectRecord.value = "The string" } - -// Need to allow untyped data -let config = JsonConfig.create(allowUntyped = true) - -let json = Json.serializeEx config data -// json is """{"value": "The string"}""" -// value was serialized as string because it was assigned to string -``` - -#### Deserialization of obj - -When deserializing obj FSharp.Json assumes the type from JSON. - -See example below: - -```fsharp -#r "FSharp.Json.dll" -open FSharp.Json - -// Record type with obj member -type ObjectRecord = { - value: obj -} - -// value is assigned to string -let json = """{"value": "The string"}""" - -// Need to allow untyped data -let config = JsonConfig.create(allowUntyped = true) - -let data = Json.deserializeEx config json -// data is { ObjectRecord.value = "The string" } -// value was deserialized as string because it was string in JSON -``` - -## Release Notes - -Could be found [here](RELEASE_NOTES.md). - -## Contributing and copyright - -The project is hosted on [GitHub][gh] where you can [report issues][issues], fork -the project and submit pull requests. If you're adding a new public API, please also -consider adding documentation to this [README][readme]. - -The library is available under Public Domain license, which allows modification and -redistribution for both commercial and non-commercial purposes. For more information see the -[License file][license] in the GitHub repository. - - [readme]: tree/master/README.md - [gh]: https://github.com/vsapronov/FSharp.Json - [issues]: https://github.com/vsapronov/FSharp.Json/issues - [license]: https://github.com/vsapronov/FSharp.Json/blob/master/LICENSE.txt - -## Maintainer(s) - -- [@vsapronov](https://github.com/vsapronov) +# FSharp.Json: JSON Serialization Library +[![GitHub Release](https://img.shields.io/github/v/release/NicoVIII/FSharp.Json?include_prereleases&sort=semver)](https://github.com/NicoVIII/FSharp.Json/releases/latest) +[![Build](https://github.com/NicoVIII/FSharp.Json/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/NicoVIII/FSharp.Json/actions/workflows/build.yml) +![Last commit](https://img.shields.io/github/last-commit/NicoVIII/FSharp.Json) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://raw.githubusercontent.com/NicoVIII/FSharp.Json/main/LICENSE.txt) + +FSharp.Json is F# JSON serialization library based on Reflection it's written in F# for F#. + +## Basic Usage Example + +Here's basic example of FSharp.Json usage: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +// Your record type +type RecordType = { + stringMember: string + intMember: int +} + +let data: RecordType = { stringMember = "The string"; intMember = 123 } + +// serialize record into JSON +let json = Json.serialize data +printfn "%s" json +// json is """{ "stringMember": "The string", "intMember": 123 }""" + +// deserialize from JSON to record +let deserialized = Json.deserialize json +printfn "%A" deserialized +// deserialized is {stringMember = "some value"; intMember = 123;} +``` + +## Table of Contents + - [Basic Usage Example](#basic-usage-example) + - [Table of Contents](#table-of-contents) + - [Why?](#why) + - [How?](#how) + - [Documentation](#documentation) + - [API Overview](#api-overview) + - [Advanced functions](#advanced-functions) + - [Configuration](#configuration) + - [Unformatted JSON](#unformatted-json) + - [Supported Types](#supported-types) + - [Customizing JSON Fields Names](#customizing-json-fields-names) + - [Change JSON field name](#change-json-field-name) + - [Change all fields names](#change-all-fields-names) + - [Null Safety](#null-safety) + - [Deserialization of null](#deserialization-of-null) + - [Customization of null deserialization](#customization-of-null-deserialization) + - [Serialization of None](#serialization-of-none) + - [Customization of None serialization](#customization-of-none-serialization) + - [Enums](#enums) + - [Customizing enum serialization](#customizing-enum-serialization) + - [Default enum behaviour](#default-enum-behaviour) + - [Unions](#unions) + - [Changing union case key](#changing-union-case-key) + - [Union case without fields](#union-case-without-fields) + - [Single case union](#single-case-union) + - [Union modes](#union-modes) + - [Type Transform](#type-transform) + - [DateTime as epoch time](#datetime-as-epoch-time) + - [System.Uri as string](#systemuri-as-string) + - [Transform by default](#transform-by-default) + - [Untyped Data](#untyped-data) + - [Serialization of obj](#serialization-of-obj) + - [Deserialization of obj](#deserialization-of-obj) + - [Release Notes](#release-notes) + - [Contributing and copyright](#contributing-and-copyright) + - [Maintainer(s)](#maintainers) + +## Why? + +Why we need yet another F# JSON serialization library? +Well, if you happy with the library that you are using currently then probably you do not need another one. +There are several available options to choose from: + + * [JSON Type Provider](http://fsharp.github.io/FSharp.Data/library/JsonProvider.html) + * [Json.Net aka Newtonsoft.Json](https://www.newtonsoft.com/json) + * [Chiron](https://github.com/xyncro/chiron) + * [JsonFSharp](https://github.com/PeteProgrammer/JsonFSharp) + * [Thoth.Json](https://mangelmaxime.github.io/Thoth/json/v2.html#code-sample) + * [FSharpLu.Json](https://github.com/Microsoft/fsharplu/tree/master/FSharpLu.Json) + * Let me know what library I missed here + +While all of these libraries do provide some good functionality all of them seem to have a weakness. +Some of them are written in C# without out of the box support for F# types. +Additionally null safety is alien concept in C# however it is a must in F#. +Other libraries provide only low-level functionality leaving a lot of cumbersome coding work to library user. +The type provider library forces developer to define JSON schema in form of json examples which is far from convenient way of defining schemas. + +FSharp.Json was developed with these values in mind: + + * Easy to use API + * Automatic mapping of F# types into JSON + * Out of the box support for F# types + * Null safety + +## How? + +FSharp.Json is pure F# library. +It uses [Reflection][reflection] to get information about F# types at runtime. +The code from [FSharp.Data library][fsharp_data] is used for parsing JSON. +The [JsonValue type][jsonvalue_type] is internalized in the FSharp.Json library. +The core of FSharp.Json library is located in single [Core.fs file][core]. + + [reflection]: https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/reflection + [fsharp_data]: http://fsharp.github.io/FSharp.Data/ + [jsonvalue_type]: http://fsharp.github.io/FSharp.Data/reference/fsharp-data-jsonvalue.html + [core]: FSharp.Json/Core.fs + +## Documentation + +This document describe all details of FSharp.Json library. The source code also has thorough documentation in comments to main types. Each feature of FSharp.Json is thoroughly covered by [unit tests](FSharp.Json.Tests). + +## API Overview + +Most of API functions are defined in [Json module](FSharp.Json/Interface.fs). + +Easiest way to serialize is to call `Json.serialize` function. +It serializes any supported F# type to string containing JSON. + +Similarly easiest way to deserialize is to call `Json.deserialize<'T>` function. +It takes string containing JSON and returns instance of type 'T. + +#### Advanced functions + +Functions `Json.serialize` and `Json.deserialize` are using default configuration. +Whenever custom configuration should be used following functions are useful: + + * `Json.serializeEx` + * `Json.deserializeEx<'T>` + +Prefix `Ex` stands for "extended". Both of these functions take [JsonConfig](FSharp.Json/InterfaceTypes.fs) instance as a first parameter. + +#### Configuration + +[JsonConfig](FSharp.Json/InterfaceTypes.fs) represents global configuration of serialization. +There's convenient way to override default configuration by using `JsonConfig.create` function. +All parameters of the function are optional and those that are provided override default values. + +For example, custom `jsonFieldNaming` could be found [here](#change-all-fields-names). + +#### Unformatted JSON + +Some products like [Apache Spark](https://spark.apache.org/) require unformatted JSON in a single line. +It is usefull to produce unformatted single line JSON in some other scenarios. +There is a function to produce unformatted JSON: `Json.serializeU`. +`U` stands for "unformatted". It has the same signature as `Json.serialize` function. +The function is a shorthand to using `unformatted` member on [JsonConfig](FSharp.Json/InterfaceTypes.fs). + +## Supported Types + +Here's full list of F# types that are supported by FSharp.Json library. + +| F# Type | JSON Type | +|:---|:---| +sbyte
byte
int16
uint16
int
uint
int64
uint64
bigint
single
float
decimal | number +string | string +char | string +bool | bool +DateTime | string according to [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)
number epoch time using [transform](#transform) +DateTimeOffset | string according to [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)
number epoch time using [transform](#transform) +Guid | string +Uri| string using [transform](#transform) +Enum | string enum value name
number enum value
read [Enums](#enums) section +option | option is not represented by itself
`None` value might be represented as `null` or omitted
read more in [Null Safety](#null-safety) section +tuple | list +record | object +map | object +array
list
set | list +union | object with special structure
read more in [Unions](#unions) section +obj | read [Untyped Data](#untyped-data) section + +## Customizing JSON Fields Names + +When record type instance is serialized into JSON members names are used as JSON fields names. +In some scenarios the JSON should have different fields names then F# record type. +This section describes how FSharp.Json library provides JSON customization abilities. + +#### Change JSON field name + +JSON field name could be easily customized with JsonField attribute: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +// attribute stringMember with JsonField +type RecordType = { + [] + stringMember: string + intMember: int +} + +let data: RecordType = { stringMember = "The string"; intMember = 123 } + +let json = Json.serialize data +printfn "%s" json +// json is """{ "different_name": "The string", "intMember": 123 }""" +// pay attention that "different_name" was used as JSON field name + +let deserialized = Json.deserialize json +printfn "%A" deserialized +// deserialized is {stringMember = "some value"; intMember = 123;} +``` + +In example above JsonField attribute affects both serialization and deserialization. + +#### Change all fields names + +What if all fields names should be different in JSON compared to F# member names? +This could be needed for example if naming convention used in F# does not match JSON naming convention. +FSharp.Json allows to map fields names with naming function. + +In the example below all camel case F# record members are mapped into snake case JSON fields: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +// attribute stringMember with JsonField +type RecordType = { + stringMember: string + intMember: int +} + +let data: RecordType = { stringMember = "The string"; intMember = 123 } + +// create config with JSON field naming setting +let config = JsonConfig.create(jsonFieldNaming = Json.snakeCase) + +let json = Json.serializeEx config data +printfn "%s" json +// json is """{ "string_member": "The string", "int_member": 123 }""" +// pay attention that "string_member" and "int_member" are in snake case + +let deserialized = Json.deserializeEx config json +printfn "%A" deserialized +// deserialized is {stringMember = "some value"; intMember = 123;} +``` + +The `Json` module defines two naming functions that could be used out of the box: snakeCase and lowerCamelCase. +The one can define it's own naming rule - it's just a function `string -> string`. + +## Null Safety + +FSharp.Json is null safe. +This means that library will never deserialize JSON into anything with null value. +FSharp.Json treats option types as an instruction that value could be null in JSON. +For example member of type `string` can't get null value in FSharp.Json, however `string option` member can have `None` value which is translated into null in JSON. +Same logic applies to all types supported by FSharp.Json library. +See examples in sections below. + +#### Deserialization of null + +In the example below `stringMember` member can't be assigned to null even though F# allows string to be null: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +type RecordType = { + stringMember: string +} + +let json = """{"stringMember":null}""" + +// this attempt to deserialize will throw exception +let deserialized = Json.deserialize json +``` + +What if the member `stringMember` could be null in JSON? +In such case the record type should explictly use string option type: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +type RecordType = { + stringMember: string option +} + +let json = """{"stringMember":null}""" + +// this attempt to deserialize will work fine +let deserialized = Json.deserialize json + +// deserialized.stringMember equals to None +printfn "%A" deserialized +``` + +If value could be null or missing in JSON then F# type used for corresponding member should be option. + +#### Customization of null deserialization + +What is the difference between missing field in JSON and field assigned to null? +By default FSharp.Json library treats both cases as None, however you can customize this logic. +Behaviour that is used to treat option types could be configured: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +type RecordType = { + stringMember: string option +} + +// this will work fine by default even when option field is missing +let deserialized1 = Json.deserialize "{}" + +printfn "%A" deserialized1 +// deserialized1.stringMember equals to None + +// create config that require option None to be represented as null +let config = JsonConfig.create(deserializeOption = DeserializeOption.RequireNull) + +// this will throw exception since config in use requires null for None deserialization. +let deserialized2 = Json.deserializeEx config "{}" +``` + +#### Serialization of None +If some member of `option` type has None value it will be serialized into JSON null by default: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +type RecordType = { + stringMember: string option +} + +// stringMember has None value +let data = { RecordType.stringMember = None } +let json = Json.serialize data +printfn "%s" json +// json is: """{ "stringMember": null }""" +``` + +#### Customization of None serialization +The one can control how None values are respresented in JSON through config. +Example belows shows how to omit members with None values: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +type RecordType = { + stringMember: string option +} + +// stringMember has None value +let data = { RecordType.stringMember = None } + +// create config that omits null values +let config = JsonConfig.create(serializeNone = SerializeNone.Omit) + +let json = Json.serializeEx config data +printfn "%s" json +// json is: """{}""" +``` + +## Enums + +By default enum value is represented as `string` that is enum member name. + +Check example code below: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +type NumberEnum = +| One = 1 +| Two = 2 +| Three = 3 + +// value will be represented as enum value name in JSON +type TheNumberEnum = { + value: NumberEnum +} + +let data = { TheNumberEnum.value = NumberEnum.Three } + +let json = Json.serialize data +// json is """{"value":"Three"}""" + +let deserialized = Json.deserialize json +// data is { TheNumberEnum.value = NumberEnum.Three } +``` + +#### Customizing enum serialization + +EnumValue member of [JsonField](FSharp.Json/InterfaceTypes.fs) attribute could be used to change serialization of enums. +There are two [modes](FSharp.Json/InterfaceTypes.fs) supported currently: enum value name and enum value. + +Here's an example of custom enum serialization: +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +type NumberEnum = +| One = 1 +| Two = 2 +| Three = 3 + +// value will be represented as enum value in JSON +type TheAttributedNumberEnum = { + [] + value: NumberEnum +} + +let data = { TheNumberEnum.value = NumberEnum.Three } + +let json = Json.serialize data +// json is """{"value":3}""" + +let deserialized = Json.deserialize json +// data is { TheNumberEnum.value = NumberEnum.Three } +``` + +#### Default enum behaviour + +Sometimes it's needed always serialize enum value as it's value. +Annotating each member of any enum type would be cumbersome. +[JsonConfig]() allows to override default enum behaviour. + +Check the example below: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +type NumberEnum = +| One = 1 +| Two = 2 +| Three = 3 + +// value will be represented as enum value name in JSON +type TheNumberEnum = { + value: NumberEnum +} + +let data = { TheNumberEnum.value = NumberEnum.Three } + +// create configuration instructing to serialize enum as enum value +let config = JsonConfig.create(enumValue = EnumMode.Value) + +let json = Json.serializeEx config data +// json is """{"value":3}""" +// value was serialized as enum value which is 3 + +let deserialized = Json.deserializeEx config json +// data is { TheNumberEnum.value = NumberEnum.Three } +``` + +## Unions + +JSON format does not support any data structure similiar to [F# discriminated unions](https://fsharpforfunandprofit.com/posts/discriminated-unions/). +Though it is still possible to represent union in JSON in some reasonable way. +By deafault FSharp.Json serializes union into JSON object with the only one field. +Name of the field corresponds to the union case. Value of the field corresponds to the union case value. + +Here's some example of default union serialization: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +type TheUnion = +| OneFieldCase of string +| ManyFieldsCase of string*int + +let data = OneFieldCase "The string" + +let json = Json.serialize data +// json is """{"OneFieldCase":"The string"}""" + +let deserialized = Json.deserialize json +// deserialized is OneFieldCase("The string") +``` + +#### Changing union case key + +The string that represents union case key could be changed with [JsonUnionCase attribute](FSharp.Json/InterfaceTypes.fs). + +See the example below: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +// OneFieldCase is attributed to be "case1" in JSON +type TheUnion = +| [] OneFieldCase of string +| ManyFieldsCase of string*int + +let data = OneFieldCase "The string" + +let json = Json.serialize data +// json is """{"case1":"The string"}""" + +let deserialized = Json.deserialize json +// deserialized is OneFieldCase("The string") +``` + +#### Single case union + +Single case union is a special scenario. +Read [here](https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/) about single case union usage. +In such case serializing union as JSON object is overkill. +It's more convenient to represent single case union the same way as a wrapped type. + +Here's example of single case union serialization: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +// Single case union type +type TheUnion = SingleCase of string + +type TheRecord = { + // value will be just a string - wrapped into union type + value: TheUnion +} + +let data = { TheRecord.value = SingleCase "The string" } + +let json = Json.serialize data +// json is """{"value":"The string"}""" + +let deserialized = Json.deserialize json +// deserialized is { TheRecord.value = SingleCase "The string" } +``` + +#### Union case without fields + +When union case does not have fields then the union value is represented by string value of the case name itself. + +Here's example of serialization union case without fields: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +// Case NoFieldCase does not have any fields +type TheUnion = +| NoFieldCase +| SingleCase of string + +type TheRecord = { + // value will be a string represting NoFieldCase + value: TheUnion +} + +let data = { TheRecord.value = NoFieldCase } + +let json = Json.serialize data +// json is """{"value":"NoFieldCase"}""" + +let deserialized = Json.deserialize json +// deserialized is { TheRecord.value = NoFieldCase } +``` + +#### Union modes + +There's [union mode](FSharp.Json/InterfaceTypes.fs) that represents union as JSON object with two fields. +One field is for case key and another one is for case value. This mode is called "case key as a field value" +If this mode is used then names of these two field should be provided through [JsonUnion attribute](FSharp.Json/InterfaceTypes.fs). + +See the example below: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +// The union will be represented as JSON object with two fields: casekey and casevalue. +[] +type TheUnion = +| OneFieldCase of string +| ManyFieldsCase of string*int + +let data = OneFieldCase "The string" + +let json = Json.serialize data +// json is """{"casekey":"OneFieldCase", "casevalue":"The string"}""" + +let deserialized = Json.deserialize json +// deserialized is OneFieldCase("The string") +``` + +## Type Transform + +[Supported types](#supported-types) section maps F# types into JSON types. +What if some data needed to be represented as a different type then the default JSON type? +If changing type of the member in F# is not an option then type transform can help. + +Any data member is translated F# Type -> JSON type by [default](#supported-types) types mapping. +[Type Transform](FSharp.Json/InterfaceTypes.fs) is applied in the middle of this translation: F# Type -> Alternative F# Type -> JSON type. +Alternative F# Type -> JSON type is still done by default types mapping, type transform is responsible for F# Type -> Alternative F# Type. + +The [Transforms](FSharp.Json/Transforms.fs) module contains transforms that are defined by FSharp.Json library. +You can define your own transforms by implementing [ITypeTransform interface](FSharp.Json/InterfaceTypes.fs). + +#### DateTime as epoch time + +Let's imagine that some DateTime member should be represented as [epoch time](https://en.wikipedia.org/wiki/Unix_time) in JSON. +Epoch time is int64 however it is still convenient to work with DateTime in F# code. +In such case [DateTimeEpoch transform](FSharp.Json/Transforms.fs) is useful. + +Here's an example of DateTimeEpoch transform usage: + +```fsharp +#r "FSharp.Json.dll" +open System +open FSharp.Json + +// value will be represented as epoch time in JSON +type DateTimeRecord = { + [)>] + value: DateTime +} + +let json = Json.serialize { DateTimeRecord.value = new DateTime(2017, 11, 5, 22, 50, 45) } +// json is """{"value":1509922245}""" + +let deserialized = Json.deserialize json +// deserialized is { DateTimeRecord.value = new DateTime(2017, 11, 5, 22, 50, 45) } +``` + +#### System.Uri as string + +This transformer transforms a Uri to/from a string for serialization. On deserialization, the string value is +handed to the System.Uri constructor. When the URI string is malformed, a UriFormatException might be thrown. + +Example use: + +```fsharp +#r "FSharp.Json.dll" +open System +open FSharp.Json + +// value will be represented as epoch time in JSON +type UriRecord = { + [)>] + value: Uri +} + +let json = Json.serialize { UriRecord.value = Uri("http://localhost:8080/") } +// json is """{"value":"http://localhost:8080/"}""" + +let deserialized = Json.deserialize json +// deserialized is { UriRecord.value = Uri("http://localhost:8080/") } +``` + +#### Transform by default + +To be developed.... + +## Untyped Data + +Using obj type in F# code is bad code smell. +Though FSharp.Json can serialize and deserialize structures without type information. +For allowing obj type in serialization/deserialization allowUntyped flag should be set to `true` on [JsonConfig](FSharp.Json/InterfaceTypes.fs). + +#### Serialization of obj + +When serializing obj FSharp.Json uses real run time type. + +Check this example: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +// Record type with obj member +type ObjectRecord = { + value: obj +} + +// Set string value to obj member +let data = { ObjectRecord.value = "The string" } + +// Need to allow untyped data +let config = JsonConfig.create(allowUntyped = true) + +let json = Json.serializeEx config data +// json is """{"value": "The string"}""" +// value was serialized as string because it was assigned to string +``` + +#### Deserialization of obj + +When deserializing obj FSharp.Json assumes the type from JSON. + +See example below: + +```fsharp +#r "FSharp.Json.dll" +open FSharp.Json + +// Record type with obj member +type ObjectRecord = { + value: obj +} + +// value is assigned to string +let json = """{"value": "The string"}""" + +// Need to allow untyped data +let config = JsonConfig.create(allowUntyped = true) + +let data = Json.deserializeEx config json +// data is { ObjectRecord.value = "The string" } +// value was deserialized as string because it was string in JSON +``` + +## Release Notes + +Could be found [here](CHANGELOG.md). + +## Contributing and copyright + +The upstream project is hosted on [GitHub][gh] where you can [report issues][issues], fork +the project and submit pull requests. If you're adding a new public API, please also +consider adding documentation to this [README][readme]. + +The library is available under Public Domain license, which allows modification and +redistribution for both commercial and non-commercial purposes. For more information see the +[License file][license] in the GitHub repository. + + [readme]: README.md + [gh]: https://github.com/vsapronov/FSharp.Json + [issues]: https://github.com/vsapronov/FSharp.Json/issues + [license]: LICENSE.txt + +## Maintainer(s) + +- [@vsapronov](https://github.com/vsapronov) (upstream) +- [@NicoVIII](https://github.com/NicoVIII) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md deleted file mode 100644 index 4ca1a45..0000000 --- a/RELEASE_NOTES.md +++ /dev/null @@ -1,42 +0,0 @@ -# Release Notes - -## 0.4.1 -* Refactored union support. - -## 0.4.0 -* Fixed no fields union case (de)serialization - not backward compatible. - -## 0.3.7 -* Added support for numeric types: byte, sbyte, int16, uint16, uint, uint64, bigint -* Added support for floating point single type - -## 0.3.6 -* Documentation cleanup - -## 0.3.5 -* Moved to Release build - -## 0.3.4 -* Moved to .NET Standard - -## 0.3.3 -* Added .NET Core support - -## 0.3.2 -* Added Transform for Uri type - -## 0.3.1 -* Fixed FSharp.Core dependency to allow newer versions - -## 0.3 -* Fix for tuples containing option types -* Support for char type -* Support for enums based on byte and char types -* Configurable enum mode -* Configurable unformatted setting - -## 0.2 -* Single case union as wrapped type - -## 0.1 -* Initial release \ No newline at end of file diff --git a/pack.fsx b/pack.fsx new file mode 100755 index 0000000..ccf5b78 --- /dev/null +++ b/pack.fsx @@ -0,0 +1,63 @@ +#!/bin/dotnet fsi + +#r "nuget: Fake.Core.ReleaseNotes" +#r "nuget: Fake.DotNet.Cli" + +// Settings +let projectName = "FSharp.Json" +let nugetDir = "./out" + +let authors = + [ "vsapronov"; "NicoVIII" ] |> String.concat ";" + +let summary = + "F# JSON Reflection based serialization library" + +let tags = "F# JSON serialization" +let copyright = "Copyright 2019" +let license = "Apache-2.0" + +let gitOwner = "NicoVIII" +let gitName = projectName +let gitHome = "https://github.com/" + gitOwner +let gitUrl = gitHome + "/" + gitName + +module Changelog = + open Fake.Core + + let changelogFilename = "CHANGELOG.md" + let changelog = Changelog.load changelogFilename + let latestEntry = changelog.LatestEntry + + let nugetVersion = latestEntry.NuGetVersion + + let packageReleaseNotes = + sprintf "%s/blob/v%s/CHANGELOG.md" gitUrl nugetVersion + +module Pack = + open Fake.DotNet + + let options (options: DotNet.PackOptions) = + let properties = + [ ("Version", Changelog.nugetVersion) + ("Authors", authors) + ("PackageProjectUrl", gitUrl) + ("PackageTags", tags) + ("RepositoryType", "git") + ("RepositoryUrl", gitUrl) + ("PackageLicenseExpression", license) + ("Copyright", copyright) + ("PackageDescription", summary) + ("PackageReleaseNotes", Changelog.packageReleaseNotes) + ("EnableSourceLink", "true") ] + + { options with + OutputPath = Some nugetDir + MSBuildParams = + { options.MSBuildParams with + Properties = properties } } + + let execute () = + DotNet.pack options $"{projectName}/{projectName}.fsproj" + +Pack.execute () diff --git a/paket.dependencies b/paket.dependencies new file mode 100644 index 0000000..8c2b095 --- /dev/null +++ b/paket.dependencies @@ -0,0 +1,8 @@ +source https://www.nuget.org/api/v2 + +framework: auto-detect + +nuget FSharp.Core 4.3.4 +nuget Microsoft.NET.Test.Sdk 15.6.2 +nuget Nunit 3.13.2 +nuget NUnit3TestAdapter 3.17.0 diff --git a/paket.lock b/paket.lock new file mode 100644 index 0000000..d306f22 --- /dev/null +++ b/paket.lock @@ -0,0 +1,284 @@ +RESTRICTION: || (== net5.0) (== netstandard2.0) +NUGET + remote: https://www.nuget.org/api/v2 + FSharp.Core (4.3.4) + Microsoft.CodeCoverage (16.9.4) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= net45)) (&& (== netstandard2.0) (>= netcoreapp1.0)) + Microsoft.DotNet.InternalAbstractions (1.0) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.AppContext (>= 4.1) + System.Collections (>= 4.0.11) + System.IO (>= 4.1) + System.IO.FileSystem (>= 4.0.1) + System.Reflection.TypeExtensions (>= 4.1) + System.Runtime.Extensions (>= 4.1) + System.Runtime.InteropServices (>= 4.1) + System.Runtime.InteropServices.RuntimeInformation (>= 4.0) + Microsoft.NET.Test.Sdk (15.6.2) + Microsoft.CodeCoverage (>= 1.0.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= net45)) (&& (== netstandard2.0) (>= netcoreapp1.0)) + Microsoft.TestPlatform.TestHost (>= 15.6.2) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp1.0)) + Microsoft.NETCore.Platforms (5.0.2) + Microsoft.NETCore.Targets (5.0) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.TestPlatform.ObjectModel (16.9.4) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp1.0)) + NuGet.Frameworks (>= 5.0) + System.Reflection.Metadata (>= 1.6) + Microsoft.TestPlatform.TestHost (16.9.4) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp1.0)) + Microsoft.TestPlatform.ObjectModel (>= 16.9.4) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp1.0)) (&& (== netstandard2.0) (>= uap10.0)) + Newtonsoft.Json (>= 9.0.1) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp1.0)) (&& (== netstandard2.0) (>= uap10.0)) + Microsoft.Win32.Primitives (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + Microsoft.Win32.Registry (5.0) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Security.AccessControl (>= 5.0) + System.Security.Principal.Windows (>= 5.0) + NETStandard.Library (2.0.3) + Microsoft.NETCore.Platforms (>= 1.1) + Newtonsoft.Json (13.0.1) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp1.0)) + NuGet.Frameworks (5.9.1) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp1.0)) + NUnit (3.13.2) + NETStandard.Library (>= 2.0) + NUnit3TestAdapter (3.17) + Microsoft.DotNet.InternalAbstractions (>= 1.0) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.ComponentModel.EventBasedAsync (>= 4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.ComponentModel.TypeConverter (>= 4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Diagnostics.Process (>= 4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Reflection (>= 4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Runtime.InteropServices.RuntimeInformation (>= 4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Threading.Thread (>= 4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Xml.XmlDocument (>= 4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Xml.XPath.XmlDocument (>= 4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + runtime.native.System (4.3.1) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1.1) + Microsoft.NETCore.Targets (>= 1.1.3) + System.AppContext (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Runtime (>= 4.3) + System.Collections (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Collections.NonGeneric (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Collections.Specialized (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Collections.NonGeneric (>= 4.3) + System.Globalization (>= 4.3) + System.Globalization.Extensions (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.ComponentModel (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Runtime (>= 4.3) + System.ComponentModel.EventBasedAsync (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.ComponentModel.Primitives (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.ComponentModel (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.ComponentModel.TypeConverter (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Collections (>= 4.3) + System.Collections.NonGeneric (>= 4.3) + System.Collections.Specialized (>= 4.3) + System.ComponentModel (>= 4.3) + System.ComponentModel.Primitives (>= 4.3) + System.Globalization (>= 4.3) + System.Linq (>= 4.3) + System.Reflection (>= 4.3) + System.Reflection.Extensions (>= 4.3) + System.Reflection.Primitives (>= 4.3) + System.Reflection.TypeExtensions (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Diagnostics.Debug (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Diagnostics.Process (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.Win32.Primitives (>= 4.3) + Microsoft.Win32.Registry (>= 4.3) + runtime.native.System (>= 4.3) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.IO.FileSystem (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Text.Encoding.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Threading.Thread (>= 4.3) + System.Threading.ThreadPool (>= 4.3) + System.Globalization (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Globalization.Extensions (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + System.Globalization (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.IO (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.IO.FileSystem (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.IO (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.IO.FileSystem.Primitives (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Runtime (>= 4.3) + System.Linq (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Reflection (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.IO (>= 4.3) + System.Reflection.Primitives (>= 4.3) + System.Runtime (>= 4.3) + System.Reflection.Extensions (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Reflection (>= 4.3) + System.Runtime (>= 4.3) + System.Reflection.Metadata (5.0) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp1.0)) + System.Reflection.Primitives (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Reflection.TypeExtensions (4.7) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Resources.ResourceManager (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Globalization (>= 4.3) + System.Reflection (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime (4.3.1) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1.1) + Microsoft.NETCore.Targets (>= 1.1.3) + System.Runtime.Extensions (4.3.1) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1.1) + Microsoft.NETCore.Targets (>= 1.1.3) + System.Runtime (>= 4.3.1) + System.Runtime.Handles (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Runtime.InteropServices (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Reflection (>= 4.3) + System.Reflection.Primitives (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Runtime.InteropServices.RuntimeInformation (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + runtime.native.System (>= 4.3) + System.Reflection (>= 4.3) + System.Reflection.Extensions (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Threading (>= 4.3) + System.Security.AccessControl (5.0) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 5.0) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.0)) + System.Security.Principal.Windows (>= 5.0) + System.Security.Principal.Windows (5.0) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Text.Encoding (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Text.Encoding.Extensions (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Text.RegularExpressions (4.3.1) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Runtime (>= 4.3.1) + System.Threading (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Runtime (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Threading.Tasks (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + Microsoft.NETCore.Platforms (>= 1.1) + Microsoft.NETCore.Targets (>= 1.1) + System.Runtime (>= 4.3) + System.Threading.Tasks.Extensions (4.5.4) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Threading.Thread (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Runtime (>= 4.3) + System.Threading.ThreadPool (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Runtime (>= 4.3) + System.Runtime.Handles (>= 4.3) + System.Xml.ReaderWriter (4.3.1) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.IO.FileSystem (>= 4.3) + System.IO.FileSystem.Primitives (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Runtime.InteropServices (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Text.Encoding.Extensions (>= 4.3) + System.Text.RegularExpressions (>= 4.3) + System.Threading.Tasks (>= 4.3) + System.Threading.Tasks.Extensions (>= 4.3) + System.Xml.XmlDocument (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Text.Encoding (>= 4.3) + System.Threading (>= 4.3) + System.Xml.ReaderWriter (>= 4.3) + System.Xml.XPath (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Collections (>= 4.3) + System.Diagnostics.Debug (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Xml.ReaderWriter (>= 4.3) + System.Xml.XPath.XmlDocument (4.3) - restriction: || (== net5.0) (&& (== netstandard2.0) (>= netcoreapp2.1)) + System.Collections (>= 4.3) + System.Globalization (>= 4.3) + System.IO (>= 4.3) + System.Resources.ResourceManager (>= 4.3) + System.Runtime (>= 4.3) + System.Runtime.Extensions (>= 4.3) + System.Threading (>= 4.3) + System.Xml.ReaderWriter (>= 4.3) + System.Xml.XmlDocument (>= 4.3) + System.Xml.XPath (>= 4.3)