diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml
index 8bef85221..5e8a40e98 100644
--- a/.azure-pipelines/ci-build.yml
+++ b/.azure-pipelines/ci-build.yml
@@ -21,6 +21,8 @@ pr:
variables:
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
+ NuGetOrganizationName: 'openapinet'
+ privateFeedBaseUrl: 'https://microsoftgraph.pkgs.visualstudio.com/0985d294-5762-4bc2-a565-161ef349ca3e/_packaging/GraphDeveloperExperiences_Public'
ProductBinPath: '$(Build.SourcesDirectory)\src\Microsoft.OpenApi\bin\$(BuildConfiguration)'
REGISTRY: 'msgraphprodregistry.azurecr.io'
IMAGE_NAME: 'public/openapi/hidi'
@@ -69,7 +71,7 @@ extends:
- task: UseDotNet@2
displayName: 'Use .NET 10'
inputs:
- version: 10.x
+ useGlobalJson: true
# Install the nuget tool.
- task: NuGetToolInstaller@1
@@ -79,6 +81,20 @@ extends:
checkLatest: true
# Build the Product project
+ - task: NuGetAuthenticate@1
+ displayName: 'Authenticate to Azure Artifacts'
+
+ - pwsh: |
+ @"
+
+
+
+
+
+
+
+ "@ | Set-Content -Path "$(Build.SourcesDirectory)/nuget.config" -Encoding UTF8
+ displayName: 'Create nuget.config (central feed)'
- task: DotNetCoreCLI@2
displayName: 'build'
inputs:
@@ -86,14 +102,18 @@ extends:
arguments: '--configuration $(BuildConfiguration) --no-incremental'
# Run the Unit test
- - task: DotNetCoreCLI@2
+ - pwsh: |
+ dotnet test "$(Build.SourcesDirectory)\Microsoft.OpenApi.slnx" `
+ --configuration $(BuildConfiguration) `
+ --no-build `
+ -- `
+ --report-azdo `
+ --publish-azdo-test-results `
+ --results-directory="$(Agent.TempDirectory)\TestResults" `
+ --minimum-expected-tests 1
displayName: 'test'
- inputs:
- command: test
- projects: '$(Build.SourcesDirectory)\Microsoft.OpenApi.slnx'
- arguments: '--configuration $(BuildConfiguration) --no-build'
- - task: EsrpCodeSigning@5
+ - task: EsrpCodeSigning@6
displayName: 'ESRP CodeSigning binaries'
inputs:
ConnectedServiceName: 'Federated DevX ESRP Managed Identity Connection'
@@ -159,7 +179,7 @@ extends:
- pwsh: dotnet pack $(Build.SourcesDirectory)/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -o $(Build.ArtifactStagingDirectory) --configuration $(BuildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg
displayName: 'pack Hidi'
- - task: EsrpCodeSigning@5
+ - task: EsrpCodeSigning@6
displayName: 'ESRP CodeSigning Nuget Packages'
inputs:
ConnectedServiceName: 'Federated DevX ESRP Managed Identity Connection'
@@ -198,7 +218,7 @@ extends:
displayName: publish Hidi as executable
inputs:
command: 'publish'
- arguments: -c Release --runtime win-x64 /p:PublishSingleFile=true /p:PackAsTool=false --self-contained --output $(Build.ArtifactStagingDirectory)/Microsoft.OpenApi.Hidi
+ arguments: -c Release --runtime win-x64 -p:RestoreConfigFile=$(Build.SourcesDirectory)\nuget.config /p:PublishSingleFile=true /p:PackAsTool=false --self-contained --output $(Build.ArtifactStagingDirectory)/Microsoft.OpenApi.Hidi
projects: 'src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj'
publishWebProjects: False
zipAfterPublish: false
@@ -208,7 +228,16 @@ extends:
inputs:
targetFolder: $(Build.ArtifactStagingDirectory)/Nugets
sourceFolder: $(Build.ArtifactStagingDirectory)
- content: '*.nupkg'
+ Contents: |
+ *.nupkg
+ *.snupkg
+
+ - task: CopyFiles@2
+ displayName: 'Include version-check script in Nugets artifact'
+ inputs:
+ SourceFolder: '$(Build.SourcesDirectory)/scripts'
+ Contents: 'check-nuget-package-published.ps1'
+ TargetFolder: '$(Build.ArtifactStagingDirectory)/Nugets/scripts'
# Copy repository files to be used in the deploy stage
- task: CopyFiles@2
@@ -247,13 +276,46 @@ extends:
pool:
vmImage: ubuntu-latest
steps:
- - task: 1ES.PublishNuget@1
- displayName: 'NuGet push'
+ - task: PowerShell@2
+ displayName: 'Check whether NuGet package version already published (idempotent)'
+ inputs:
+ targetType: filePath
+ filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1'
+ arguments: '-PackageId "Microsoft.OpenApi.Hidi" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"'
+ pwsh: true
+ env:
+ FEED_ACCESS_TOKEN: $(System.AccessToken)
+ - task: CopyFiles@2
+ displayName: 'Stage Hidi NuGet packages for ESRP release'
+ condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
+ inputs:
+ SourceFolder: '$(Pipeline.Workspace)'
+ Contents: |
+ Microsoft.OpenApi.Hidi.*.nupkg
+ Microsoft.OpenApi.Hidi.*.snupkg
+ TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/hidi'
+ CleanTargetFolder: true
+ - task: EsrpRelease@14
+ displayName: 'ESRP Release - Hidi NuGet'
+ condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
inputs:
- packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.Hidi.*.nupkg'
- packageParentPath: '$(Pipeline.Workspace)'
- nuGetFeedType: external
- publishFeedCredentials: 'OpenAPI Nuget Connection'
+ connectedservicename: 'Federated DevX ESRP Managed Identity Connection'
+ usemanagedidentity: false
+ keyvaultname: 'akv-prod-eastus'
+ authcertname: 'ReferenceLibraryPrivateCert'
+ signcertname: 'ReferencePackagePublisherCertificate'
+ clientid: '65035b7f-7357-4f29-bf25-c5ee5c3949f8'
+ intent: 'packagedistribution'
+ contenttype: 'NuGet'
+ organizationname: '$(NuGetOrganizationName)'
+ contentsource: 'Folder'
+ folderlocation: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/hidi'
+ waitforreleasecompletion: true
+ owners: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
+ approvers: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
+ serviceendpointurl: 'https://api.esrp.microsoft.com/'
+ mainpublisher: 'ESRPRELPACMAN'
+ domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2'
- deployment: deploy_lib
condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded())
@@ -272,21 +334,48 @@ extends:
pool:
vmImage: ubuntu-latest
steps:
- - pwsh: |
- $fileNames = "$(Pipeline.Workspace)/Microsoft.OpenApi.Hidi.*.nupkg", "$(Pipeline.Workspace)/Microsoft.OpenApi.YamlReader.*.nupkg", "$(Pipeline.Workspace)/Microsoft.OpenApi.Workbench.*.nupkg"
- foreach($fileName in $fileNames) {
- if(Test-Path $fileName) {
- Remove-Item $fileName -Verbose
- }
- }
- displayName: remove other nupkgs to avoid duplication
- - task: 1ES.PublishNuget@1
- displayName: 'NuGet push'
+ - task: PowerShell@2
+ displayName: 'Check whether NuGet package version already published (idempotent)'
inputs:
- packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.*.nupkg'
- packageParentPath: '$(Pipeline.Workspace)'
- nuGetFeedType: external
- publishFeedCredentials: 'OpenAPI Nuget Connection'
+ targetType: filePath
+ filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1'
+ arguments: '-PackageId "Microsoft.OpenApi" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"'
+ pwsh: true
+ env:
+ FEED_ACCESS_TOKEN: $(System.AccessToken)
+ - task: CopyFiles@2
+ displayName: 'Stage OpenAPI NuGet packages for ESRP release'
+ condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
+ inputs:
+ SourceFolder: '$(Pipeline.Workspace)'
+ Contents: |
+ Microsoft.OpenApi.*.nupkg
+ Microsoft.OpenApi.*.snupkg
+ !Microsoft.OpenApi.Hidi.*
+ !Microsoft.OpenApi.YamlReader.*
+ TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/openapi'
+ CleanTargetFolder: true
+ - task: EsrpRelease@14
+ displayName: 'ESRP Release - OpenAPI NuGet'
+ condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
+ inputs:
+ connectedservicename: 'Federated DevX ESRP Managed Identity Connection'
+ usemanagedidentity: false
+ keyvaultname: 'akv-prod-eastus'
+ authcertname: 'ReferenceLibraryPrivateCert'
+ signcertname: 'ReferencePackagePublisherCertificate'
+ clientid: '65035b7f-7357-4f29-bf25-c5ee5c3949f8'
+ intent: 'packagedistribution'
+ contenttype: 'NuGet'
+ organizationname: '$(NuGetOrganizationName)'
+ contentsource: 'Folder'
+ folderlocation: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/openapi'
+ waitforreleasecompletion: true
+ owners: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
+ approvers: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
+ serviceendpointurl: 'https://api.esrp.microsoft.com/'
+ mainpublisher: 'ESRPRELPACMAN'
+ domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2'
- deployment: deploy_yaml_reader
condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded())
@@ -305,13 +394,46 @@ extends:
pool:
vmImage: ubuntu-latest
steps:
- - task: 1ES.PublishNuget@1
- displayName: 'NuGet push'
+ - task: PowerShell@2
+ displayName: 'Check whether NuGet package version already published (idempotent)'
+ inputs:
+ targetType: filePath
+ filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1'
+ arguments: '-PackageId "Microsoft.OpenApi.YamlReader" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"'
+ pwsh: true
+ env:
+ FEED_ACCESS_TOKEN: $(System.AccessToken)
+ - task: CopyFiles@2
+ displayName: 'Stage YAML reader NuGet packages for ESRP release'
+ condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
inputs:
- packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.YamlReader.*.nupkg'
- packageParentPath: '$(Pipeline.Workspace)'
- nuGetFeedType: external
- publishFeedCredentials: 'OpenAPI Nuget Connection'
+ SourceFolder: '$(Pipeline.Workspace)'
+ Contents: |
+ Microsoft.OpenApi.YamlReader.*.nupkg
+ Microsoft.OpenApi.YamlReader.*.snupkg
+ TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/yaml-reader'
+ CleanTargetFolder: true
+ - task: EsrpRelease@14
+ displayName: 'ESRP Release - YAML reader NuGet'
+ condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
+ inputs:
+ connectedservicename: 'Federated DevX ESRP Managed Identity Connection'
+ usemanagedidentity: false
+ keyvaultname: 'akv-prod-eastus'
+ authcertname: 'ReferenceLibraryPrivateCert'
+ signcertname: 'ReferencePackagePublisherCertificate'
+ clientid: '65035b7f-7357-4f29-bf25-c5ee5c3949f8'
+ intent: 'packagedistribution'
+ contenttype: 'NuGet'
+ organizationname: '$(NuGetOrganizationName)'
+ contentsource: 'Folder'
+ folderlocation: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/yaml-reader'
+ waitforreleasecompletion: true
+ owners: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
+ approvers: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
+ serviceendpointurl: 'https://api.esrp.microsoft.com/'
+ mainpublisher: 'ESRPRELPACMAN'
+ domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2'
- deployment: create_github_release
condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded())
@@ -441,7 +563,33 @@ extends:
displayName: 'Get current date'
name: setdate
condition: eq(variables['Build.SourceBranch'], variables['PREVIEW_BRANCH'])
-
+
+ # Keep feed credentials out of the Docker build context and image layers.
+ - pwsh: |
+ if ([string]::IsNullOrWhiteSpace($env:FEED_ACCESS_TOKEN)) {
+ throw "No Azure Artifacts access token available for the Docker build."
+ }
+ $feedAccessToken = [System.Security.SecurityElement]::Escape($env:FEED_ACCESS_TOKEN)
+ @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "@ | Set-Content -Path "$(Agent.TempDirectory)/hidi-docker.nuget.config" -Encoding UTF8
+ displayName: 'Create Docker NuGet config (central feed)'
+ env:
+ FEED_ACCESS_TOKEN: $(System.AccessToken)
+
- script: |
docker run --privileged --rm msgraphprodregistry.azurecr.io/tonistiigi/binfmt --install all
displayName: "Enable multi-platform builds"
@@ -460,6 +608,7 @@ extends:
# Using quotes around tags to prevent flag interpretation
docker buildx build \
--platform linux/amd64,linux/arm64/v8 \
+ --secret id=nuget_config,src="$(Agent.TempDirectory)/hidi-docker.nuget.config" \
--push \
-t "$(REGISTRY)/$(IMAGE_NAME):nightly" \
-t "$(REGISTRY)/$(IMAGE_NAME):${VERSION}.${BUILDDATE}${RUNNUMBER}" \
@@ -472,6 +621,7 @@ extends:
echo "Building Docker image for release..."
docker buildx build\
--platform linux/amd64,linux/arm64/v8 \
+ --secret id=nuget_config,src="$(Agent.TempDirectory)/hidi-docker.nuget.config" \
--push \
-t "$(REGISTRY)/$(IMAGE_NAME):latest" \
-t "$(REGISTRY)/$(IMAGE_NAME):${VERSION}" \
@@ -479,6 +629,14 @@ extends:
displayName: 'Build and Push Release Image'
condition: contains(variables['Build.SourceBranch'], 'refs/tags/v')
+ - pwsh: |
+ $configPath = "$(Agent.TempDirectory)/hidi-docker.nuget.config"
+ if (Test-Path $configPath) {
+ Remove-Item $configPath -Force
+ }
+ displayName: 'Remove Docker NuGet config'
+ condition: always()
+
# once the nuget has been released, fill this form to get the public documentation updated.
# https://dev.azure.com/msft-skilling/Content/_workitems/create/User%20Story?templateId=39fb91e3-64a2-4c8a-83db-b2bdf3603dd3&ownerId=c4a28f90-17ae-4384-b514-7273392b082b
# https://learn.microsoft.com/en-us/dotnet/api/microsoft.openapi
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..16abc6aef
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,11 @@
+{
+ "name": "OpenAPI.NET",
+ "image": "mcr.microsoft.com/devcontainers/dotnet:10.0",
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-dotnettools.csdevkit"
+ ]
+ }
+ }
+}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index c3da60bcb..22ae8e30f 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,30 +1,48 @@
version: 2
updates:
- - package-ecosystem: "github-actions"
- # default location of `.github/workflows`
- directory: "/"
- open-pull-requests-limit: 10
- schedule:
- interval: "daily"
- - package-ecosystem: "nuget"
- # location of package manifests
- directory: "/"
- open-pull-requests-limit: 10
- schedule:
- interval: "daily"
- groups:
- MicrosoftExtensions:
- patterns:
- - "Microsoft.Extensions.*"
- coverlet:
- patterns:
- - "coverlet.*"
- - package-ecosystem: dotnet-sdk
- directory: /
- schedule:
- interval: "daily"
- ignore:
- - dependency-name: '*'
- update-types:
- - version-update:semver-major
- - version-update:semver-minor
+- package-ecosystem: github-actions
+ directory: /
+ open-pull-requests-limit: 10
+ schedule:
+ interval: daily
+ groups:
+ codeql:
+ patterns:
+ - github/codeql-action*
+ cooldown:
+ default-days: 7
+- package-ecosystem: nuget
+ directory: /
+ open-pull-requests-limit: 10
+ schedule:
+ interval: daily
+ groups:
+ MicrosoftExtensions:
+ patterns:
+ - Microsoft.Extensions.*
+ testing:
+ patterns:
+ - coverlet.*
+ - Microsoft.NET.Test.Sdk
+ - Microsoft.Testing.*
+ - xunit.*
+ cooldown:
+ default-days: 7
+- package-ecosystem: dotnet-sdk
+ directory: /
+ schedule:
+ interval: daily
+ ignore:
+ - dependency-name: '*'
+ update-types:
+ - version-update:semver-major
+ - version-update:semver-minor
+ cooldown:
+ default-days: 7
+- package-ecosystem: devcontainers
+ directory: /
+ open-pull-requests-limit: 10
+ schedule:
+ interval: daily
+ cooldown:
+ default-days: 7
diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml
index 9d9039433..f6dafa170 100644
--- a/.github/workflows/auto-merge-dependabot.yml
+++ b/.github/workflows/auto-merge-dependabot.yml
@@ -19,14 +19,14 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
- uses: dependabot/fetch-metadata@v3.1.0
+ uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
# Only if version bump is not a major version change
if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}}
- run: gh pr merge --auto --merge "$PR_URL"
+ run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index fb99868ee..4ae9e6ae0 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -18,18 +18,18 @@ jobs:
GITHUB_RUN_NUMBER: ${{ github.run_number }}
steps:
- name: Setup .NET 8
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 8.x
- name: Setup .NET 10
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.x
- name: Checkout repository
id: checkout_repo
- uses: actions/checkout@v6
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
@@ -45,7 +45,7 @@ jobs:
id: run_unit_tests
shell: pwsh
run: |
- dotnet test Microsoft.OpenApi.slnx -c Release --no-build -v n --collect:"XPlat Code Coverage"
+ dotnet test --solution Microsoft.OpenApi.slnx -c Release -v n --results-directory=./TestResults --coverlet --coverlet-output-format cobertura --report-gh
- name: Install report generator
shell: pwsh
@@ -55,7 +55,7 @@ jobs:
- name: Generate coverage report
shell: pwsh
run: |
- reportgenerator -reports:**/coverage.cobertura.xml -targetdir:./reports/coverage -reporttypes:"Html;MarkdownSummaryGithub;Cobertura"
+ reportgenerator -reports:./TestResults/**/coverage.cobertura.*.xml -targetdir:./reports/coverage -reporttypes:"Html;MarkdownSummaryGithub;Cobertura"
- name: Add coverage to job summary
shell: bash
@@ -64,14 +64,14 @@ jobs:
- name: Upload coverage report
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') || (github.event_name != 'pull_request' && github.ref_name == github.event.repository.default_branch)
- uses: actions/upload-code-coverage@v1
+ uses: actions/upload-code-coverage@2b21a77928be8d5168c2b9581a67f2adbebacc52 # v1.4.4
with:
file: ./reports/coverage/Cobertura.xml
language: CSharp
label: code-coverage/dotnet
- name: Upload coverage artifact
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage
path: reports/coverage
@@ -80,15 +80,15 @@ jobs:
name: Validate Project for Trimming
runs-on: windows-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup .NET 8
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 8.x
- name: Setup .NET 10
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.x
@@ -102,15 +102,15 @@ jobs:
needs: [ci]
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup .NET 8
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 8.x
- name: Setup .NET 10
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.x
@@ -125,7 +125,7 @@ jobs:
working-directory: ./performance/benchmark
- name: Publish benchmark results
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
if-no-files-found: error
name: benchmark-results
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 8ae3c621e..b2311adf8 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,21 +20,21 @@ jobs:
steps:
- name: Checkout repository
id: checkout_repo
- uses: actions/checkout@v6
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup .NET 8
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 8.0.x
- name: Setup .NET 10
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x
- name: Initialize CodeQL
id: init_codeql
- uses: github/codeql-action/init@v4
+ uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
queries: security-and-quality
@@ -54,6 +54,6 @@ jobs:
- name: Perform CodeQL Analysis
id: analyze_codeql
- uses: github/codeql-action/analyze@v4
+ uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
# Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)
\ No newline at end of file
diff --git a/.github/workflows/promote-shipped-apis.yml b/.github/workflows/promote-shipped-apis.yml
index 569a73905..825b8944f 100644
--- a/.github/workflows/promote-shipped-apis.yml
+++ b/.github/workflows/promote-shipped-apis.yml
@@ -7,6 +7,10 @@ on:
- support/v2
workflow_dispatch:
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
jobs:
promote-apis:
runs-on: ubuntu-latest
@@ -16,13 +20,22 @@ jobs:
steps:
- name: Generate GitHub App token
id: app-token
- uses: actions/create-github-app-token@v3
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.RELEASE_PLEASE_TOKEN_PROVIDER_APP_ID }}
private-key: ${{ secrets.RELEASE_PLEASE_TOKEN_PROVIDER_PEM }}
+ permission-contents: write
+ permission-pull-requests: write
+
+ - name: Get GitHub App user ID
+ id: get-user-id
+ shell: bash
+ env:
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
+ run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
@@ -32,9 +45,9 @@ jobs:
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
- git config --global user.name "github-actions[bot]"
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
- git config --global url."https://$($env:GH_TOKEN)@github.com/".insteadOf "https://github.com/"
+ git config --global user.name "${{ steps.app-token.outputs.app-slug }}[bot]"
+ git config --global user.email "${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com"
+ git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/"
- name: Check for existing PR
id: check_pr
diff --git a/.github/workflows/release-please-gha.yml b/.github/workflows/release-please-gha.yml
index a6fdc1927..74a6233fb 100644
--- a/.github/workflows/release-please-gha.yml
+++ b/.github/workflows/release-please-gha.yml
@@ -34,7 +34,7 @@ jobs:
if: needs.check-secret.outputs.has-token == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Validate PublicAPI.Unshipped.txt files are empty
shell: pwsh
@@ -62,13 +62,13 @@ jobs:
- name: Generate GitHub App token
id: app-token
- uses: actions/create-github-app-token@v3
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.RELEASE_PLEASE_TOKEN_PROVIDER_APP_ID }}
private-key: ${{ secrets.RELEASE_PLEASE_TOKEN_PROVIDER_PEM }}
- name: Release Please
- uses: googleapis/release-please-action@v5
+ uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
token: ${{ steps.app-token.outputs.token }}
config-file: release-please-config.json
diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index fe5afd83a..a6954d70d 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -35,23 +35,23 @@ jobs:
runs-on: windows-latest
steps:
- name: Set up JDK 17
- uses: actions/setup-java@v5
+ uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
- distribution: 'adopt'
+ distribution: 'temurin'
java-version: 17
- name: Setup .NET 8
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 8.0.x
- name: Setup .NET 10
- uses: actions/setup-dotnet@v5
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x
- - uses: actions/checkout@v6
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Cache SonarCloud packages
- uses: actions/cache@v5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
@@ -61,12 +61,10 @@ jobs:
- name: Build and analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
- CollectCoverage: true
- CoverletOutputFormat: 'opencover' # https://github.com/microsoft/vstest/issues/4014#issuecomment-1307913682
shell: pwsh
run: |
- dotnet tool run dotnet-sonarscanner begin /k:"microsoft_OpenAPI.NET" /o:"microsoft" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="test/**/coverage.opencover.xml"
+ dotnet tool run dotnet-sonarscanner begin /k:"microsoft_OpenAPI.NET" /o:"microsoft" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover*.xml"
dotnet workload restore
dotnet build
- dotnet test Microsoft.OpenApi.slnx --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
- dotnet tool run dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
\ No newline at end of file
+ dotnet test --solution Microsoft.OpenApi.slnx --verbosity normal --coverlet --coverlet-output-format opencover --report-gh
+ dotnet tool run dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
diff --git a/.gitignore b/.gitignore
index 258fee87a..09353aba3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,7 +127,7 @@ nCrunchTemp_*
*.mm.*
AutoTest.Net/
-# Web workbench (sass)
+# Sass cache
.sass-cache/
# Installshield output folder
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f391d4162..f7929cb4c 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "3.6.0"
+ ".": "3.10.2"
}
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 66912fac4..ad2bdf11d 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -49,23 +49,7 @@
"console": "internalConsole",
"stopAtEntry": false,
"requireExactSource": false,
- },
- {
- // Use IntelliSense to find out which attributes exist for C# debugging
- // Use hover for the description of the existing attributes
- // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/main/debugger-launchjson.md
- "name": "Launch Workbench",
- "type": "coreclr",
- "request": "launch",
- "preLaunchTask": "build",
- // If you have changed target frameworks, make sure to update the program path.
- "program": "${workspaceFolder}/src/Microsoft.OpenApi.WorkBench/bin/Debug/net8.0-windows/Microsoft.OpenApi.Workbench.exe",
- "args": [],
- "cwd": "${workspaceFolder}/src/Microsoft.OpenApi.Workbench",
- // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
- "console": "internalConsole",
- "stopAtEntry": false
- },
+ },
{
"name": ".NET Core Attach",
"type": "coreclr",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 186b10bea..619467ff3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,6 +6,7 @@
},
"cSpell.words": [
"csdl",
- "Hidi"
+ "Hidi",
+ "Xunit"
]
}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index e68597ddc..c61ccd31c 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -43,12 +43,6 @@
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
- },
- {
- "label": "workbench",
- "type": "shell",
- "command": "src/Microsoft.OpenApi.WorkBench/bin/Debug/Microsoft.OpenApi.WorkBench.exe",
- "problemMatcher": []
}
]
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f370a06c8..0077ae140 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,89 @@
# Changelog
+## [3.10.2](https://github.com/microsoft/OpenAPI.NET/compare/v3.10.1...v3.10.2) (2026-08-20)
+
+
+### Bug Fixes
+
+* duplicate schema example when serializing to v2 ([#3045](https://github.com/microsoft/OpenAPI.NET/issues/3045)) ([50f5a2b](https://github.com/microsoft/OpenAPI.NET/commit/50f5a2bb119089bb48987494d906cb1a95cddbd1))
+
+## [3.10.1](https://github.com/microsoft/OpenAPI.NET/compare/v3.10.0...v3.10.1) (2026-08-19)
+
+
+### Bug Fixes
+
+* circular ref guard ([#3033](https://github.com/microsoft/OpenAPI.NET/issues/3033)) ([8018d40](https://github.com/microsoft/OpenAPI.NET/commit/8018d40aac5fc6ed5e022e6203ee917ecf36472c))
+* harden yaml parsing ([#3027](https://github.com/microsoft/OpenAPI.NET/issues/3027)) ([e0d5e9d](https://github.com/microsoft/OpenAPI.NET/commit/e0d5e9d105ad1a7b29ff6c628793614f5610f048))
+
+## [3.10.0](https://github.com/microsoft/OpenAPI.NET/compare/v3.9.0...v3.10.0) (2026-08-11)
+
+
+### Features
+
+* do not ignore multiple types when serializing to 3.0 ([#2960](https://github.com/microsoft/OpenAPI.NET/issues/2960)) ([ebaf27a](https://github.com/microsoft/OpenAPI.NET/commit/ebaf27ac46f8820e7b817cc33e693632c2fc908a))
+
+
+### Bug Fixes
+
+* bound YAML anchor/alias expansion to prevent OOM (billion laughs) ([#3000](https://github.com/microsoft/OpenAPI.NET/issues/3000)) ([2179326](https://github.com/microsoft/OpenAPI.NET/commit/21793261d6860fb6e607b71fd95c0bd320724764))
+
+## [3.9.0](https://github.com/microsoft/OpenAPI.NET/compare/v3.8.0...v3.9.0) (2026-07-15)
+
+
+### Features
+
+* adds support for anchor and id external resolution ([1591007](https://github.com/microsoft/OpenAPI.NET/commit/15910070f2bf3006f79e1c43dd9c2b56e45d665c))
+* **schema:** resolve bare $dynamicRef via $dynamicAnchor index ([#2913](https://github.com/microsoft/OpenAPI.NET/issues/2913)) ([eacc2fc](https://github.com/microsoft/OpenAPI.NET/commit/eacc2fc06537fbb11fe95765e4fb6df09861c9af))
+* support relative URI resolution in $dynamicRef ([#2928](https://github.com/microsoft/OpenAPI.NET/issues/2928)) ([#2945](https://github.com/microsoft/OpenAPI.NET/issues/2945)) ([821053b](https://github.com/microsoft/OpenAPI.NET/commit/821053ba02129e92868df2ae0c26551fb3d5276a))
+
+
+### Bug Fixes
+
+* adds explicit error message for invalid json pointers ([63fc55d](https://github.com/microsoft/OpenAPI.NET/commit/63fc55d69274ebd405e677983e0a72efd2b96079))
+* adds explicit error message for invalid json pointers ([bc93efe](https://github.com/microsoft/OpenAPI.NET/commit/bc93efe9d4694e89da1ea30fbc2d031e7f1318ae))
+* default mapping is not being serialized with the correct shape ([fe4a25f](https://github.com/microsoft/OpenAPI.NET/commit/fe4a25f1f25c853404540d30f11812f1208f93c9))
+* differentiate unset value from null value in OpenApiSchema.Const ([#2936](https://github.com/microsoft/OpenAPI.NET/issues/2936)) ([07b525f](https://github.com/microsoft/OpenAPI.NET/commit/07b525f568b65b5003deaccc56829f5e7e8e2641))
+* handle nullability more accurately during serialization for 3.0/2.0 ([#2933](https://github.com/microsoft/OpenAPI.NET/issues/2933)) ([0ace243](https://github.com/microsoft/OpenAPI.NET/commit/0ace243ebbabe82aacc52d49fe58f54f039bdf76))
+* validate required properties of security scheme before serialization ([#2952](https://github.com/microsoft/OpenAPI.NET/issues/2952)) ([f31b192](https://github.com/microsoft/OpenAPI.NET/commit/f31b192e9d59d39671fdfd03b4fa4d309b03021b))
+
+## [3.8.0](https://github.com/microsoft/OpenAPI.NET/compare/v3.7.0...v3.8.0) (2026-07-03)
+
+
+### Features
+
+* add JsonConverter for OpenApiSchema System.Text.Json serialization ([#2915](https://github.com/microsoft/OpenAPI.NET/issues/2915)) ([2f8b3d2](https://github.com/microsoft/OpenAPI.NET/commit/2f8b3d2a160f5b44b5aeaa3d83ff2025302ce1e2))
+* **library:** support schema keywords on references ([434b2f8](https://github.com/microsoft/OpenAPI.NET/commit/434b2f8b34e4a3a27bd99e43f61692f0b00c3054))
+* **library:** support schema keywords on references ([66a9d04](https://github.com/microsoft/OpenAPI.NET/commit/66a9d04036556945ee3222849b7d071a30016b81)), closes [#2903](https://github.com/microsoft/OpenAPI.NET/issues/2903)
+
+
+### Bug Fixes
+
+* Don't silently skip null assignment to OpenApiDocument.Tags ([3764142](https://github.com/microsoft/OpenAPI.NET/commit/37641424837df0b5e142ca130cb16df699bdb09f))
+* handling of nullable enums for 3.0 ([#2920](https://github.com/microsoft/OpenAPI.NET/issues/2920)) ([beb68f5](https://github.com/microsoft/OpenAPI.NET/commit/beb68f52b86c1437e50f7561d9be1ebdbe146963))
+* **library:** keep v3 schema references ref-only ([c938727](https://github.com/microsoft/OpenAPI.NET/commit/c9387274be6a6955952174d677bea0a35bda2624))
+* preserve JSON Schema 2020-12 keyword siblings on $ref schemas for OAS 3.1+ ([#2896](https://github.com/microsoft/OpenAPI.NET/issues/2896)) ([08160c8](https://github.com/microsoft/OpenAPI.NET/commit/08160c872a5f931eecd12446335c2f51fdad083e))
+* use async method for crypto flush ([6e675d9](https://github.com/microsoft/OpenAPI.NET/commit/6e675d943537312386c9683790848ad1cbacb713))
+
+## [3.7.0](https://github.com/microsoft/OpenAPI.NET/compare/v3.6.0...v3.7.0) (2026-06-10)
+
+
+### Features
+
+* add contains/minContains/maxContains members ([78475e3](https://github.com/microsoft/OpenAPI.NET/commit/78475e38f7c61e349f2d2ad477b5a96a9c3df848))
+* add contains/minContains/maxContains members ([1a974f8](https://github.com/microsoft/OpenAPI.NET/commit/1a974f8dfcd7850c70d80133ceecee08f6671cd7))
+* **library:** add missing json schema properties ([9b1aed6](https://github.com/microsoft/OpenAPI.NET/commit/9b1aed61041551b06926fc2ee4e12705190e16b3))
+* **library:** add missing json schema properties ([82f84e0](https://github.com/microsoft/OpenAPI.NET/commit/82f84e072d9f6b4b5907e9435212fbfc8f82d9c7))
+
+
+### Bug Fixes
+
+* **library:** always copy unevaluated properties ([4907d1c](https://github.com/microsoft/OpenAPI.NET/commit/4907d1c1c4d5b8450d32459752e64e5beb592f46))
+* **library:** avoid false circular refs for external schema re-exports ([b635242](https://github.com/microsoft/OpenAPI.NET/commit/b635242a402c3c468eb86dcf2b6d94a05717d92e))
+* **library:** avoid false circular refs for external schema re-exports ([7a443c2](https://github.com/microsoft/OpenAPI.NET/commit/7a443c298fb87346338095b5df86f6608fb4d582))
+* **library:** remove unshipped schema extension fallback ([cf54bb3](https://github.com/microsoft/OpenAPI.NET/commit/cf54bb3e2746c0f6c7a60fde5bcc7fa1139dd8b6))
+* **library:** use version-specific schema keyword callbacks ([6e22ec6](https://github.com/microsoft/OpenAPI.NET/commit/6e22ec6948509d2e256932ee55f1781a544cb53f))
+* **library:** use x-jsonschema schema extensions ([eb1891a](https://github.com/microsoft/OpenAPI.NET/commit/eb1891a8d77915add1cdd88949b40aa43cd525b8))
+
## [3.6.0](https://github.com/microsoft/OpenAPI.NET/compare/v3.5.5...v3.6.0) (2026-06-01)
diff --git a/Directory.Build.props b/Directory.Build.props
index 781552888..1971fa2c6 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,17 +8,19 @@
https://github.com/Microsoft/OpenAPI.NET
https://github.com/microsoft/OpenAPI.NET/releases
true
+ OpenSource
+ 2027-09
http://go.microsoft.com/fwlink/?LinkID=288890
https://github.com/Microsoft/OpenAPI.NET
© Microsoft Corporation. All rights reserved.
OpenAPI .NET
- 3.6.0
+ 3.10.2
true
-
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 25f1ec589..4e932b975 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,13 @@
-FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build-env
WORKDIR /app
COPY ./src ./hidi/src
COPY ./Directory.Build.props ./hidi/Directory.Build.props
COPY ./README.md ./hidi/README.md
WORKDIR /app/hidi
-RUN dotnet publish ./src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -c Release
+# CI supplies the private feed config as a secret; local builds use default NuGet sources.
+RUN --mount=type=secret,id=nuget_config,target=/app/hidi/NuGet.Config \
+ dotnet publish ./src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -c Release
FROM mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled AS runtime
WORKDIR /app
diff --git a/Microsoft.OpenApi.slnx b/Microsoft.OpenApi.slnx
index f764776bf..8356bb428 100644
--- a/Microsoft.OpenApi.slnx
+++ b/Microsoft.OpenApi.slnx
@@ -9,7 +9,6 @@
-
diff --git a/README.md b/README.md
index 3c77f909f..8622d64d8 100644
--- a/README.md
+++ b/README.md
@@ -91,22 +91,6 @@ In order to test the validity of an OpenApi document, we avail the following too
A commandline tool for validating and transforming OpenAPI descriptions. [Installation guidelines and documentation](https://github.com/microsoft/OpenAPI.NET/blob/main/src/Microsoft.OpenApi.Hidi/readme.md)
-- Microsoft.OpenApi.Workbench
-
- A workbench tool consisting of a GUI where you can test and convert OpenAPI descriptions in both JSON and YAML from v2-->v3 and vice versa.
-
- #### Installation guidelines:
- 1. Clone the repo locally by running this command:
- `git clone https://github.com/microsoft/OpenAPI.NET.git`
- 2. Open the solution file `(.slnx)` in the root of the project with Visual Studio
- 3. Navigate to the `src/Microsoft.OpenApi.Workbench` directory and set it as the startup project
- 4. Run the project and you'll see a GUI pop up resembling the one below:
-
-
- 
-
- 5. Copy and paste your OpenAPI descriptions in the **Input Content** window or paste the path to the descriptions file in the **Input File** textbox and click on `Convert` to render the results.
-
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
diff --git a/agents.md b/agents.md
new file mode 100644
index 000000000..abeed736e
--- /dev/null
+++ b/agents.md
@@ -0,0 +1,7 @@
+# Regex handling
+
+- Analyze regex patterns for matching complexity and excessive backtracking, including on long and near-matching inputs. Prefer equivalent patterns with less backtracking when available, while preserving matching semantics and target-framework compatibility.
+- For fixed patterns on modern targets, use source-generated regexes with explicit match timeouts (`GeneratedRegex` under `NET8_0_OR_GREATER`).
+- Use conditional compilation to provide a regular `Regex` with the same pattern and an explicit match timeout for older targets. Do not duplicate regex validation with a manually maintained character scanner.
+- Older-runtime regex matching may still time out under load because timeouts use wall-clock time. If consumers encounter this limitation, recommend upgrading to a modern runtime that uses the source-generated implementation.
+- Keep shared patterns in constants and reference those constants in validation diagnostics and tests.
diff --git a/docs/images/workbench.png b/docs/images/workbench.png
deleted file mode 100644
index 898fe9b5b..000000000
Binary files a/docs/images/workbench.png and /dev/null differ
diff --git a/global.json b/global.json
index d895dfe5a..7494875e1 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,8 @@
{
"sdk": {
- "version": "10.0.300"
+ "version": "10.0.401"
+ },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
}
}
\ No newline at end of file
diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md
index 9b5931f81..95726f79a 100644
--- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md
+++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md
@@ -1,20 +1,20 @@
```
-BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8457/25H2/2025Update/HudsonValley2)
-Snapdragon X 12-core X1E80100 3.40 GHz (Max: 3.42GHz), 1 CPU, 12 logical and 12 physical cores
-.NET SDK 10.0.300
- [Host] : .NET 8.0.27 (8.0.27, 8.0.2726.22922), Arm64 RyuJIT armv8.0-a
- ShortRun : .NET 8.0.27 (8.0.27, 8.0.2726.22922), Arm64 RyuJIT armv8.0-a
+BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
+AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
+.NET SDK 10.0.400
+ [Host] : .NET 8.0.30 (8.0.30, 8.0.3026.36720), X64 RyuJIT x86-64-v3
+ ShortRun : .NET 8.0.30 (8.0.30, 8.0.3026.36720), X64 RyuJIT x86-64-v3
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
```
-| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
-|------------- |-------------:|--------------:|-------------:|-----------:|-----------:|----------:|-------------:|
-| PetStoreYaml | 276.3 μs | 38.27 μs | 2.10 μs | 74.2188 | 11.7188 | - | 305.91 KB |
-| PetStoreJson | 112.8 μs | 2.80 μs | 0.15 μs | 41.0156 | 0.4883 | - | 168.05 KB |
-| GHESYaml | 608,668.3 μs | 188,763.29 μs | 10,346.75 μs | 44000.0000 | 18000.0000 | 3000.0000 | 250121.85 KB |
-| GHESJson | 244,147.6 μs | 361,794.79 μs | 19,831.19 μs | 17000.0000 | 9000.0000 | 2000.0000 | 107293.42 KB |
-| GHESNextYaml | 765,440.1 μs | 23,162.26 μs | 1,269.60 μs | 79000.0000 | 20000.0000 | 3000.0000 | 443655.46 KB |
-| GHESNextJson | 435,329.2 μs | 241,612.89 μs | 13,243.62 μs | 51000.0000 | 11000.0000 | 2000.0000 | 305423.41 KB |
+| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
+|------------- |---------------:|--------------:|-------------:|-----------:|-----------:|----------:|-------------:|
+| PetStoreYaml | 569.0 μs | 108.17 μs | 5.93 μs | 19.5313 | - | - | 327.8 KB |
+| PetStoreJson | 250.8 μs | 20.85 μs | 1.14 μs | 11.7188 | 1.9531 | - | 209.67 KB |
+| GHESYaml | 831,169.7 μs | 217,393.05 μs | 11,916.05 μs | 18000.0000 | 10000.0000 | 2000.0000 | 267570.8 KB |
+| GHESJson | 367,711.4 μs | 135,955.63 μs | 7,452.19 μs | 9000.0000 | 8000.0000 | 2000.0000 | 140917.06 KB |
+| GHESNextYaml | 1,040,243.2 μs | 167,595.29 μs | 9,186.46 μs | 30000.0000 | 11000.0000 | 2000.0000 | 469507.05 KB |
+| GHESNextJson | 615,173.6 μs | 143,051.39 μs | 7,841.13 μs | 22000.0000 | 10000.0000 | 2000.0000 | 345247.91 KB |
diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv
index 6ca713e4b..bb632a223 100644
--- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv
+++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv
@@ -1,7 +1,7 @@
Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Gen0,Gen1,Gen2,Allocated
-PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,276.3 μs,38.27 μs,2.10 μs,74.2188,11.7188,0.0000,305.91 KB
-PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,112.8 μs,2.80 μs,0.15 μs,41.0156,0.4883,0.0000,168.05 KB
-GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"608,668.3 μs","188,763.29 μs","10,346.75 μs",44000.0000,18000.0000,3000.0000,250121.85 KB
-GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"244,147.6 μs","361,794.79 μs","19,831.19 μs",17000.0000,9000.0000,2000.0000,107293.42 KB
-GHESNextYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"765,440.1 μs","23,162.26 μs","1,269.60 μs",79000.0000,20000.0000,3000.0000,443655.46 KB
-GHESNextJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"435,329.2 μs","241,612.89 μs","13,243.62 μs",51000.0000,11000.0000,2000.0000,305423.41 KB
+PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,569.0 μs,108.17 μs,5.93 μs,19.5313,0.0000,0.0000,327.8 KB
+PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,250.8 μs,20.85 μs,1.14 μs,11.7188,1.9531,0.0000,209.67 KB
+GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"831,169.7 μs","217,393.05 μs","11,916.05 μs",18000.0000,10000.0000,2000.0000,267570.8 KB
+GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"367,711.4 μs","135,955.63 μs","7,452.19 μs",9000.0000,8000.0000,2000.0000,140917.06 KB
+GHESNextYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"1,040,243.2 μs","167,595.29 μs","9,186.46 μs",30000.0000,11000.0000,2000.0000,469507.05 KB
+GHESNextJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"615,173.6 μs","143,051.39 μs","7,841.13 μs",22000.0000,10000.0000,2000.0000,345247.91 KB
diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html
index a6a592c7b..c9feca148 100644
--- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html
+++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html
@@ -2,7 +2,7 @@
-performance.Descriptions-20260526-120411
+performance.Descriptions-20260819-014451
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Metro.MSControls.Toolkit.Implicit.xaml b/src/Microsoft.OpenApi.Workbench/Themes/Metro/Metro.MSControls.Toolkit.Implicit.xaml
deleted file mode 100644
index cb48bd5be..000000000
--- a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Metro.MSControls.Toolkit.Implicit.xaml
+++ /dev/null
@@ -1,939 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Styles.Shared.xaml b/src/Microsoft.OpenApi.Workbench/Themes/Metro/Styles.Shared.xaml
deleted file mode 100644
index 783d05199..000000000
--- a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Styles.Shared.xaml
+++ /dev/null
@@ -1,697 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Styles.WPF.xaml b/src/Microsoft.OpenApi.Workbench/Themes/Metro/Styles.WPF.xaml
deleted file mode 100644
index 988e787b8..000000000
--- a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Styles.WPF.xaml
+++ /dev/null
@@ -1,830 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Visible
-
-
-
-
-
-
-
-
-
-
- Visible
-
-
-
-
-
-
-
-
-
-
-
-
-
- Visible
-
-
-
-
-
-
-
-
-
-
- Visible
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Theme.Colors.xaml b/src/Microsoft.OpenApi.Workbench/Themes/Metro/Theme.Colors.xaml
deleted file mode 100644
index 02babe07a..000000000
--- a/src/Microsoft.OpenApi.Workbench/Themes/Metro/Theme.Colors.xaml
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- #FF282828
- #FF3F3F3F
- #FF565656
- #FF858585
- #FFB9B9B9
- #FFD7D7D7
- #FFE7E7E7
- #FFF4F4F4
- #FFF9F9F9
- #FFFFFFFF
-
-
- #E5FFFFFF
- #BFFFFFFF
- #99FFFFFF
- #72FFFFFF
- #4CFFFFFF
- #00FFFFFF
-
-
- #72000000
- #4C000000
- #26000000
- #00000000
- #66E2E2E2
-
-
- #FF0086AF
- #FF00AADE
- #FF80D5EF
- #FFB2E1EF
- #2600AADE
-
-
- #FFD0284C
- #FFF55E7F
- #FFFFCAD5
-
-
- #FF006481
- #FF8A9B0F
- #FF3E4700
- #FFF14D0F
- #FF8D2E00
- #FF81106B
- #FF410135
- #FFFCA910
- #FF8D4902
- #FF037A54
- #FF003F2A
- #FF154D85
- #FF02284D
- #FF543511
- #FF211303
- #FF89806D
- #FF393225
- #FF58458B
- #FF211347
- #7FB9B9B9
- #33565656
- #7F3F3F3F
- #FF686868
- #8000AADE
- #CC3F3F3F
-
-
-
- #FF0092BE
- #FF00AADE
- #FF2BB9E5
- #FF55C8EB
- #FF80D7F2
-
-
\ No newline at end of file
diff --git a/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj b/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj
index c4961cb10..5a04ce302 100644
--- a/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj
+++ b/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj
@@ -28,20 +28,12 @@
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
+
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
+
-
-
-
-
+
+
@@ -71,4 +63,4 @@
-
\ No newline at end of file
+
diff --git a/src/Microsoft.OpenApi.YamlReader/OpenApiReaderSettingsExtensions.cs b/src/Microsoft.OpenApi.YamlReader/OpenApiReaderSettingsExtensions.cs
index ee5add0a0..006d1fb3d 100644
--- a/src/Microsoft.OpenApi.YamlReader/OpenApiReaderSettingsExtensions.cs
+++ b/src/Microsoft.OpenApi.YamlReader/OpenApiReaderSettingsExtensions.cs
@@ -1,4 +1,5 @@
-using Microsoft.OpenApi.YamlReader;
+using System;
+using Microsoft.OpenApi.YamlReader;
namespace Microsoft.OpenApi.Reader;
@@ -17,4 +18,18 @@ public static void AddYamlReader(this OpenApiReaderSettings settings)
settings.TryAddReader(OpenApiConstants.Yaml, yamlReader);
settings.TryAddReader(OpenApiConstants.Yml, yamlReader);
}
+
+ ///
+ /// Adds a YAML reader for the specified format using per-reader resource limits.
+ ///
+ /// The settings to add the reader to.
+ /// The YAML reader settings.
+ public static void AddYamlReader(this OpenApiReaderSettings settings, OpenApiYamlReaderSettings yamlSettings)
+ {
+ if (settings is null) throw new ArgumentNullException(nameof(settings));
+ if (yamlSettings is null) throw new ArgumentNullException(nameof(yamlSettings));
+ var yamlReader = new OpenApiYamlReader(yamlSettings);
+ settings.TryAddReader(OpenApiConstants.Yaml, yamlReader);
+ settings.TryAddReader(OpenApiConstants.Yml, yamlReader);
+ }
}
diff --git a/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs b/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs
index 0bf2627ec..13f17d77d 100644
--- a/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs
+++ b/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs
@@ -7,9 +7,8 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.OpenApi.Reader;
-using SharpYaml.Serialization;
+using SharpYaml;
using System;
-using System.Linq;
using System.Text;
namespace Microsoft.OpenApi.YamlReader
@@ -17,10 +16,46 @@ namespace Microsoft.OpenApi.YamlReader
///
/// Reader for parsing YAML files into an OpenAPI document.
///
+ ///
+ /// Input is converted directly from SharpYaml parser events so resource limits are enforced
+ /// before SharpYaml's recursive YAML model loader can compose or expand the document.
+ ///
public class OpenApiYamlReader : IOpenApiReader
{
private const int copyBufferSize = 4096;
private static readonly OpenApiJsonReader _jsonReader = new();
+ private readonly OpenApiYamlReaderSettings _yamlSettings;
+
+ ///
+ /// Initializes a YAML reader using the current legacy global conversion limits.
+ ///
+ public OpenApiYamlReader()
+ : this(new()
+ {
+ MaxDepth = YamlConverter.MaxDepth,
+ MaxNodeCount = YamlConverter.MaxNodeCount,
+ MaxAliasExpansionNodeCount = YamlConverter.MaxAliasExpansionNodeCount,
+ })
+ {
+ }
+
+ ///
+ /// Initializes a YAML reader with immutable per-reader resource limits.
+ ///
+ /// The YAML reader settings.
+ public OpenApiYamlReader(OpenApiYamlReaderSettings settings)
+ {
+ if (settings is null) throw new ArgumentNullException(nameof(settings));
+ settings.Validate();
+ _yamlSettings = new()
+ {
+ MaxDepth = settings.MaxDepth,
+ MaxNodeCount = settings.MaxNodeCount,
+ MaxAliasExpansionNodeCount = settings.MaxAliasExpansionNodeCount,
+ MaxInputByteCount = settings.MaxInputByteCount,
+ MaxScalarLength = settings.MaxScalarLength,
+ };
+ }
///
public async Task ReadAsync(Stream input,
@@ -29,16 +64,33 @@ public async Task ReadAsync(Stream input,
CancellationToken cancellationToken = default)
{
if (input is null) throw new ArgumentNullException(nameof(input));
+ if (settings is null) throw new ArgumentNullException(nameof(settings));
if (input is MemoryStream memoryStream)
{
- return UpdateFormat(Read(memoryStream, location, settings));
+ return ReadCore(memoryStream, location, settings, cancellationToken);
}
else
{
using var preparedStream = new MemoryStream();
- await input.CopyToAsync(preparedStream, copyBufferSize, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await CopyToMemoryStreamAsync(
+ input,
+ preparedStream,
+ _yamlSettings.MaxInputByteCount,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (OpenApiReaderException ex)
+ {
+ return new()
+ {
+ Document = null,
+ Diagnostic = CreateDiagnostic(new(ex)),
+ };
+ }
+
preparedStream.Position = 0;
- return UpdateFormat(Read(preparedStream, location, settings));
+ return ReadCore(preparedStream, location, settings, cancellationToken);
}
}
@@ -46,14 +98,22 @@ public async Task ReadAsync(Stream input,
public ReadResult Read(MemoryStream input,
Uri location,
OpenApiReaderSettings settings)
+ => ReadCore(input, location, settings, CancellationToken.None);
+
+ private ReadResult ReadCore(MemoryStream input,
+ Uri location,
+ OpenApiReaderSettings settings,
+ CancellationToken cancellationToken)
{
if (input is null) throw new ArgumentNullException(nameof(input));
if (settings is null) throw new ArgumentNullException(nameof(settings));
+ cancellationToken.ThrowIfCancellationRequested();
JsonNode jsonNode;
// Parse the YAML text in the stream into a sequence of JsonNodes
try
{
+ EnsureInputWithinLimit(input, _yamlSettings.MaxInputByteCount);
#if NET
// this represents net core, net5 and up
using var stream = new StreamReader(input, default, true, -1, settings.LeaveStreamOpen);
@@ -61,22 +121,65 @@ public ReadResult Read(MemoryStream input,
// the implementation differs and results in a null reference exception in NETFX
using var stream = new StreamReader(input, Encoding.UTF8, true, 4096, settings.LeaveStreamOpen);
#endif
- jsonNode = LoadJsonNodesFromYamlDocument(stream);
+ jsonNode = LoadJsonNodesFromYamlDocument(stream, cancellationToken);
}
catch (JsonException ex)
{
- var diagnostic = new OpenApiDiagnostic();
- diagnostic.Errors.Add(new($"#line={ex.LineNumber}", ex.Message));
- diagnostic.Format = OpenApiConstants.Yaml;
return new()
{
Document = null,
- Diagnostic = diagnostic,
+ Diagnostic = CreateDiagnostic(new($"#line={ex.LineNumber}", ex.Message)),
+ };
+ }
+ catch (OpenApiReaderException ex)
+ {
+ return new()
+ {
+ Document = null,
+ Diagnostic = CreateDiagnostic(new(ex)),
};
}
+ cancellationToken.ThrowIfCancellationRequested();
return UpdateFormat(Read(jsonNode, location, settings));
}
+
+ private static async Task CopyToMemoryStreamAsync(
+ Stream input,
+ MemoryStream output,
+ uint maxInputByteCount,
+ CancellationToken cancellationToken)
+ {
+ var buffer = new byte[copyBufferSize];
+ long totalBytesRead = 0;
+ int bytesRead;
+ while ((bytesRead = await input.ReadAsync(
+ buffer,
+ 0,
+ buffer.Length,
+ cancellationToken).ConfigureAwait(false)) > 0)
+ {
+ if (bytesRead > (long)maxInputByteCount - totalBytesRead)
+ {
+ throw CreateInputLimitException(maxInputByteCount);
+ }
+
+ await output.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
+ totalBytesRead += bytesRead;
+ }
+ }
+
+ private static void EnsureInputWithinLimit(MemoryStream input, uint maxInputByteCount)
+ {
+ if (input.Length - input.Position > maxInputByteCount)
+ {
+ throw CreateInputLimitException(maxInputByteCount);
+ }
+ }
+
+ private static OpenApiReaderException CreateInputLimitException(uint maxInputByteCount)
+ => new($"The YAML input exceeds the maximum supported size of {maxInputByteCount} bytes.");
+
private static ReadResult UpdateFormat(ReadResult result)
{
result.Diagnostic ??= new OpenApiDiagnostic();
@@ -103,13 +206,22 @@ public static ReadResult Read(JsonNode jsonNode, Uri location, OpenApiReaderSett
// Parse the YAML
try
{
- using var stream = new StreamReader(input);
- jsonNode = LoadJsonNodesFromYamlDocument(stream);
+ EnsureInputWithinLimit(input, _yamlSettings.MaxInputByteCount);
+#if NET
+ using var stream = new StreamReader(input, default, true, -1, settings?.LeaveStreamOpen ?? false);
+#else
+ using var stream = new StreamReader(input, Encoding.UTF8, true, 4096, settings?.LeaveStreamOpen ?? false);
+#endif
+ jsonNode = LoadJsonNodesFromYamlDocument(stream, CancellationToken.None);
}
catch (JsonException ex)
{
- diagnostic = new();
- diagnostic.Errors.Add(new($"#line={ex.LineNumber}", ex.Message));
+ diagnostic = CreateDiagnostic(new($"#line={ex.LineNumber}", ex.Message));
+ return default;
+ }
+ catch (OpenApiReaderException ex)
+ {
+ diagnostic = CreateDiagnostic(new(ex));
return default;
}
@@ -123,20 +235,34 @@ public static ReadResult Read(JsonNode jsonNode, Uri location, OpenApiReaderSett
}
///
- /// Helper method to turn streams into a sequence of JsonNodes
+ /// Converts the first YAML document in a stream into a JSON node.
///
/// Stream containing YAML formatted text
- /// Instance of a YamlDocument
- static JsonNode LoadJsonNodesFromYamlDocument(TextReader input)
+ /// Propagates notification that parsing should be cancelled.
+ /// The converted JSON node.
+ private JsonNode LoadJsonNodesFromYamlDocument(TextReader input, CancellationToken cancellationToken)
{
- var yamlStream = new YamlStream();
- yamlStream.Load(input);
- if (yamlStream.Documents.Any() && yamlStream.Documents[0].ToJsonNode() is { } jsonNode)
+ try
+ {
+ return new YamlJsonParser(_yamlSettings).Parse(input, cancellationToken);
+ }
+ catch (YamlException ex)
{
- return jsonNode;
+ var location = ex.Start.Line >= 0
+ ? $" at line {ex.Start.Line + 1}, column {ex.Start.Column + 1}"
+ : string.Empty;
+ throw new OpenApiReaderException($"Unable to parse the YAML document{location}: {ex.Message}", ex);
}
+ }
- throw new InvalidOperationException("No documents found in the YAML stream.");
+ private static OpenApiDiagnostic CreateDiagnostic(OpenApiError error)
+ {
+ var diagnostic = new OpenApiDiagnostic
+ {
+ Format = OpenApiConstants.Yaml,
+ };
+ diagnostic.Errors.Add(error);
+ return diagnostic;
}
}
}
diff --git a/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReaderSettings.cs b/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReaderSettings.cs
new file mode 100644
index 000000000..b525f65c6
--- /dev/null
+++ b/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReaderSettings.cs
@@ -0,0 +1,73 @@
+using System;
+
+namespace Microsoft.OpenApi.YamlReader;
+
+///
+/// Configures resource limits for an .
+///
+public sealed class OpenApiYamlReaderSettings
+{
+ ///
+ /// Default maximum number of input bytes read from a single YAML document (128 MiB).
+ /// Bounds the buffered copy of a non-seekable stream, so an endless or oversized response body
+ /// cannot exhaust memory before parsing begins.
+ ///
+ public const uint DefaultMaxInputByteCount = 128 * 1024 * 1024;
+
+ ///
+ /// Default maximum length of a single YAML scalar value (65,536 UTF-16 code units).
+ /// Bounds the cost of any one key, string, number, date or block literal. For reference, the
+ /// longest scalar in the Microsoft Graph beta description is 1,833 code units, so this leaves
+ /// substantial headroom for legitimate documents.
+ ///
+ public const uint DefaultMaxScalarLength = 64 * 1024;
+
+ ///
+ /// Gets or sets the maximum YAML nesting depth.
+ /// Defaults to and cannot exceed
+ /// .
+ ///
+ public uint MaxDepth { get; set; } = YamlConverter.DefaultMaxDepth;
+
+ ///
+ /// Gets or sets the maximum number of JSON nodes materialized from one YAML document.
+ /// Defaults to and cannot exceed
+ /// .
+ ///
+ public uint MaxNodeCount { get; set; } = YamlConverter.DefaultMaxNodeCount;
+
+ ///
+ /// Gets or sets the maximum number of JSON nodes materialized specifically from aliases.
+ /// Defaults to .
+ ///
+ public uint MaxAliasExpansionNodeCount { get; set; } = YamlConverter.DefaultMaxAliasExpansionNodeCount;
+
+ ///
+ /// Gets or sets the maximum number of input bytes read from one YAML document.
+ /// Defaults to .
+ ///
+ public uint MaxInputByteCount { get; set; } = DefaultMaxInputByteCount;
+
+ ///
+ /// Gets or sets the maximum length of one YAML scalar value.
+ /// Defaults to .
+ ///
+ public uint MaxScalarLength { get; set; } = DefaultMaxScalarLength;
+
+ internal void Validate()
+ {
+ YamlConverter.ValidateMaxDepth(MaxDepth, nameof(MaxDepth));
+ YamlConverter.ValidateMaxNodeCount(MaxNodeCount, nameof(MaxNodeCount));
+ ValidatePositive(MaxAliasExpansionNodeCount, nameof(MaxAliasExpansionNodeCount));
+ ValidatePositive(MaxInputByteCount, nameof(MaxInputByteCount));
+ ValidatePositive(MaxScalarLength, nameof(MaxScalarLength));
+ }
+
+ private static void ValidatePositive(uint value, string parameterName)
+ {
+ if (value == 0)
+ {
+ throw new ArgumentOutOfRangeException(parameterName, $"{parameterName} must be greater than zero.");
+ }
+ }
+}
diff --git a/src/Microsoft.OpenApi.YamlReader/PublicAPI.Shipped.txt b/src/Microsoft.OpenApi.YamlReader/PublicAPI.Shipped.txt
index 7ea43f370..4c258a43e 100644
--- a/src/Microsoft.OpenApi.YamlReader/PublicAPI.Shipped.txt
+++ b/src/Microsoft.OpenApi.YamlReader/PublicAPI.Shipped.txt
@@ -15,3 +15,30 @@ static Microsoft.OpenApi.YamlReader.YamlConverter.ToJsonNode(this SharpYaml.Seri
static Microsoft.OpenApi.YamlReader.YamlConverter.ToJsonNode(this SharpYaml.Serialization.YamlStream! yaml) -> System.Collections.Generic.IEnumerable!
static Microsoft.OpenApi.YamlReader.YamlConverter.ToJsonObject(this SharpYaml.Serialization.YamlMappingNode! yaml) -> System.Text.Json.Nodes.JsonObject!
static Microsoft.OpenApi.YamlReader.YamlConverter.ToYamlNode(this System.Text.Json.Nodes.JsonNode! json) -> SharpYaml.Serialization.YamlNode!
+const Microsoft.OpenApi.YamlReader.YamlConverter.DefaultMaxDepth = 64 -> uint
+const Microsoft.OpenApi.YamlReader.YamlConverter.DefaultMaxNodeCount = 5000000 -> uint
+static Microsoft.OpenApi.YamlReader.YamlConverter.MaxDepth.get -> uint
+static Microsoft.OpenApi.YamlReader.YamlConverter.MaxDepth.set -> void
+static Microsoft.OpenApi.YamlReader.YamlConverter.MaxNodeCount.get -> uint
+static Microsoft.OpenApi.YamlReader.YamlConverter.MaxNodeCount.set -> void
+Microsoft.OpenApi.YamlReader.OpenApiYamlReader.OpenApiYamlReader(Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings! settings) -> void
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxAliasExpansionNodeCount.get -> uint
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxAliasExpansionNodeCount.set -> void
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxDepth.get -> uint
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxDepth.set -> void
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxInputByteCount.get -> uint
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxInputByteCount.set -> void
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxNodeCount.get -> uint
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxNodeCount.set -> void
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxScalarLength.get -> uint
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxScalarLength.set -> void
+Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.OpenApiYamlReaderSettings() -> void
+const Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.DefaultMaxInputByteCount = 134217728 -> uint
+const Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.DefaultMaxScalarLength = 65536 -> uint
+const Microsoft.OpenApi.YamlReader.YamlConverter.DefaultMaxAliasExpansionNodeCount = 5000 -> uint
+const Microsoft.OpenApi.YamlReader.YamlConverter.MaximumAllowedDepth = 256 -> uint
+const Microsoft.OpenApi.YamlReader.YamlConverter.MaximumAllowedNodeCount = 10000000 -> uint
+static Microsoft.OpenApi.Reader.OpenApiReaderSettingsExtensions.AddYamlReader(this Microsoft.OpenApi.Reader.OpenApiReaderSettings! settings, Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings! yamlSettings) -> void
+static Microsoft.OpenApi.YamlReader.YamlConverter.MaxAliasExpansionNodeCount.get -> uint
+static Microsoft.OpenApi.YamlReader.YamlConverter.MaxAliasExpansionNodeCount.set -> void
diff --git a/src/Microsoft.OpenApi.YamlReader/YamlConversionBudget.cs b/src/Microsoft.OpenApi.YamlReader/YamlConversionBudget.cs
new file mode 100644
index 000000000..4b3ed75d9
--- /dev/null
+++ b/src/Microsoft.OpenApi.YamlReader/YamlConversionBudget.cs
@@ -0,0 +1,125 @@
+namespace Microsoft.OpenApi.YamlReader;
+
+///
+/// Tracks the resource budget consumed while materializing a single YAML document.
+///
+///
+///
+/// A budget instance is scoped to one document and is not thread safe. Callers charge the budget
+/// before allocating, so a document that would breach a limit is rejected without the
+/// allocation ever happening.
+///
+///
+/// Every limit breach throws , which the reader converts into an
+/// OpenApiDiagnostic. That is the whole point of this type: hostile input produces a reportable
+/// diagnostic rather than an unrecoverable process failure.
+///
+///
+internal sealed class YamlConversionBudget
+{
+ private readonly uint _maxDepth;
+ private readonly uint _maxNodeCount;
+ private readonly uint _maxAliasExpansionNodeCount;
+ private uint _nodeCount;
+ private uint _aliasExpansionNodeCount;
+
+ ///
+ /// Initializes a budget for a single document.
+ ///
+ /// Maximum nesting depth. Bounds stack and structural growth.
+ /// Maximum total nodes materialized from the document.
+ ///
+ /// Maximum nodes materialized specifically by expanding aliases. This is the anti-amplification
+ /// limit and is deliberately far smaller than : a large document is
+ /// legitimate, but a small document that expands into a large one is not.
+ ///
+ public YamlConversionBudget(uint maxDepth, uint maxNodeCount, uint maxAliasExpansionNodeCount)
+ {
+ _maxDepth = maxDepth;
+ _maxNodeCount = maxNodeCount;
+ _maxAliasExpansionNodeCount = maxAliasExpansionNodeCount;
+ }
+
+ ///
+ /// Charges one node at the supplied depth.
+ ///
+ /// Zero-based nesting depth of the node being materialized.
+ /// The depth or total node limit would be exceeded.
+ public void EnterNode(uint depth)
+ {
+ ValidateDepth(depth);
+ AddNodes(1);
+ }
+
+ ///
+ /// Charges the full cost of expanding an alias, against both the alias budget and the total budget.
+ ///
+ /// Zero-based nesting depth at which the alias appears.
+ /// Number of nodes the alias will materialize when cloned.
+ ///
+ /// Height of the subtree the alias will materialize, where a scalar has height 1.
+ ///
+ /// The depth, alias, or total node limit would be exceeded.
+ ///
+ /// Must be called before the clone is taken. Charging afterwards would allow the very allocation
+ /// this limit exists to prevent.
+ ///
+ public void EnterAlias(uint depth, uint expandedNodeCount, uint expandedHeight)
+ {
+ ValidateDepth(depth);
+
+ // The alias site clears the depth check on its own, but expanding it grafts an entire
+ // subtree at this position. Without charging the grafted height, an anchor defined at a
+ // legal depth can be replayed from another legal depth to produce a tree deeper than the
+ // limit. The underlying YAML parser cannot catch this either, because it sees an alias as
+ // a single event and never re-walks the anchored content.
+ if (expandedHeight > _maxDepth - depth)
+ {
+ throw new OpenApiReaderException($"The YAML document expands an alias to more than the maximum supported nesting depth of {_maxDepth}.");
+ }
+
+ if (expandedNodeCount > _maxAliasExpansionNodeCount - _aliasExpansionNodeCount)
+ {
+ throw new OpenApiReaderException($"The YAML document expands aliases to more than the maximum supported number of nodes ({_maxAliasExpansionNodeCount}).");
+ }
+
+ _aliasExpansionNodeCount += expandedNodeCount;
+ AddNodes(expandedNodeCount);
+ }
+
+ ///
+ /// Validates that a node at is within the depth limit.
+ ///
+ ///
+ /// is zero-based, so a node at that depth occupies level
+ /// depth + 1. Rejecting depth >= _maxDepth therefore admits exactly
+ /// _maxDepth levels, matching the limit enforced by the underlying YAML parser.
+ /// The comparison avoids arithmetic so it cannot overflow.
+ ///
+ private void ValidateDepth(uint depth)
+ {
+ if (depth >= _maxDepth)
+ {
+ throw new OpenApiReaderException($"The YAML document exceeds the maximum supported nesting depth of {_maxDepth}.");
+ }
+ }
+
+ ///
+ /// Charges nodes against the total node budget.
+ ///
+ ///
+ /// The remaining headroom is compared as count > _maxNodeCount - _nodeCount rather than
+ /// _nodeCount + count > _maxNodeCount. Both operands are unsigned, so the latter form could
+ /// wrap and silently admit an over-budget document; the invariant _nodeCount <= _maxNodeCount
+ /// makes the subtraction used here safe from underflow.
+ ///
+ private void AddNodes(uint count)
+ {
+ if (count > _maxNodeCount - _nodeCount)
+ {
+ throw new OpenApiReaderException($"The YAML document expands to more than the maximum supported number of nodes ({_maxNodeCount}). This may indicate a YAML anchor/alias expansion attack.");
+ }
+
+ _nodeCount += count;
+ }
+}
diff --git a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs
index 1cafea77b..aa73618f5 100644
--- a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs
+++ b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Nodes;
using SharpYaml;
@@ -12,8 +13,114 @@ namespace Microsoft.OpenApi.YamlReader
///
/// Provides extensions to convert YAML models to JSON models.
///
+ ///
+ /// These limits apply after a SharpYaml model exists. Use
+ /// for untrusted input so limits are enforced before SharpYaml model loading.
+ ///
public static class YamlConverter
{
+ ///
+ /// Default maximum nesting depth allowed when converting a YAML node graph into JSON nodes.
+ /// Mirrors the default System.Text.Json depth limit (64) that already bounds the JSON reader path,
+ /// protecting the recursive conversion from stack exhaustion on deeply nested documents.
+ ///
+ public const uint DefaultMaxDepth = 64;
+
+ ///
+ /// Default maximum number of JSON nodes that may be materialized from a single YAML document.
+ /// Guards against YAML anchor/alias expansion ("billion laughs") attacks, where a tiny document
+ /// expands exponentially when its shared node graph is materialized into an independent JSON tree.
+ ///
+ public const uint DefaultMaxNodeCount = 5_000_000;
+
+ ///
+ /// Default maximum number of JSON nodes that may be materialized from YAML aliases.
+ ///
+ public const uint DefaultMaxAliasExpansionNodeCount = 5_000;
+
+ ///
+ /// Maximum configurable YAML nesting depth.
+ ///
+ public const uint MaximumAllowedDepth = 256;
+
+ ///
+ /// Maximum configurable number of JSON nodes that may be materialized from a single YAML document.
+ /// Bounds the running node totals so they cannot overflow while accumulating, which would surface as an
+ /// instead of a reportable diagnostic.
+ ///
+ public const uint MaximumAllowedNodeCount = 10_000_000;
+
+ private static uint _maxDepth = DefaultMaxDepth;
+ private static uint _maxNodeCount = DefaultMaxNodeCount;
+ private static uint _maxAliasExpansionNodeCount = DefaultMaxAliasExpansionNodeCount;
+
+ ///
+ /// Gets or sets the maximum nesting depth allowed when converting a YAML node graph into JSON nodes.
+ /// Defaults to and cannot exceed the library's safe depth ceiling.
+ ///
+ /// Thrown when set outside the supported range.
+ public static uint MaxDepth
+ {
+ get => _maxDepth;
+ set
+ {
+ ValidateMaxDepth(value, nameof(value));
+ _maxDepth = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum number of JSON nodes that may be materialized from a single YAML document.
+ /// Defaults to , guarding against YAML anchor/alias expansion
+ /// ("billion laughs") attacks. Raise this if legitimate large documents are being rejected, or lower
+ /// it to fail faster when only small documents are expected. Cannot exceed
+ /// .
+ ///
+ /// Thrown when set outside the supported range.
+ public static uint MaxNodeCount
+ {
+ get => _maxNodeCount;
+ set
+ {
+ ValidateMaxNodeCount(value, nameof(value));
+ _maxNodeCount = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum number of JSON nodes that may be materialized from YAML aliases.
+ ///
+ /// Thrown when set to zero.
+ public static uint MaxAliasExpansionNodeCount
+ {
+ get => _maxAliasExpansionNodeCount;
+ set
+ {
+ if (value == 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "MaxAliasExpansionNodeCount must be greater than zero.");
+ }
+
+ _maxAliasExpansionNodeCount = value;
+ }
+ }
+
+ internal static void ValidateMaxDepth(uint value, string parameterName)
+ {
+ if (value == 0 || value > MaximumAllowedDepth)
+ {
+ throw new ArgumentOutOfRangeException(parameterName, $"MaxDepth must be between 1 and {MaximumAllowedDepth}.");
+ }
+ }
+
+ internal static void ValidateMaxNodeCount(uint value, string parameterName)
+ {
+ if (value == 0 || value > MaximumAllowedNodeCount)
+ {
+ throw new ArgumentOutOfRangeException(parameterName, $"MaxNodeCount must be between 1 and {MaximumAllowedNodeCount}.");
+ }
+ }
+
///
/// Converts all of the documents in a YAML stream to s.
///
@@ -42,13 +149,7 @@ public static JsonNode ToJsonNode(this YamlDocument yaml)
/// Thrown for YAML that is not compatible with JSON.
public static JsonNode ToJsonNode(this YamlNode yaml)
{
- return yaml switch
- {
- YamlMappingNode map => map.ToJsonObject(),
- YamlSequenceNode seq => seq.ToJsonArray(),
- YamlScalarNode scalar => scalar.ToJsonValue(),
- _ => throw new NotSupportedException("This yaml isn't convertible to JSON")
- };
+ return CreateConversionContext().Convert(yaml, 0).Node;
}
///
@@ -79,14 +180,7 @@ public static YamlNode ToYamlNode(this JsonNode json)
///
public static JsonObject ToJsonObject(this YamlMappingNode yaml)
{
- var node = new JsonObject();
- foreach (var keyValuePair in yaml)
- {
- var key = ((YamlScalarNode)keyValuePair.Key).Value!;
- node[key] = keyValuePair.Value.ToJsonNode();
- }
-
- return node;
+ return (JsonObject)CreateConversionContext().Convert(yaml, 0).Node;
}
private static YamlMappingNode ToYamlMapping(this JsonObject obj)
@@ -104,13 +198,7 @@ private static YamlMappingNode ToYamlMapping(this JsonObject obj)
///
public static JsonArray ToJsonArray(this YamlSequenceNode yaml)
{
- var node = new JsonArray();
- foreach (var value in yaml)
- {
- node.Add(value.ToJsonNode());
- }
-
- return node;
+ return (JsonArray)CreateConversionContext().Convert(yaml, 0).Node;
}
private static YamlSequenceNode ToYamlSequence(this JsonArray arr)
@@ -126,19 +214,150 @@ private static YamlSequenceNode ToYamlSequence(this JsonArray arr)
"NULL"
};
- private static JsonValue ToJsonValue(this YamlScalarNode yaml)
+ private static YamlConversionContext CreateConversionContext()
{
- return yaml.Style switch
+ var maxDepth = MaxDepth;
+ var maxNodeCount = MaxNodeCount;
+ var maxAliasExpansionNodeCount = MaxAliasExpansionNodeCount;
+ ValidateMaxDepth(maxDepth, nameof(MaxDepth));
+ return new(
+ new YamlConversionBudget(maxDepth, maxNodeCount, maxAliasExpansionNodeCount));
+ }
+
+ internal static JsonValue ToJsonValue(string? value, ScalarStyle style)
+ {
+ return style switch
{
- ScalarStyle.Plain when decimal.TryParse(yaml.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => JsonValue.Create(d),
- ScalarStyle.Plain when bool.TryParse(yaml.Value, out var b) => JsonValue.Create(b),
- ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => (JsonValue)JsonNullSentinel.JsonNull.DeepClone(),
- ScalarStyle.Plain => JsonValue.Create(yaml.Value),
- ScalarStyle.SingleQuoted or ScalarStyle.DoubleQuoted or ScalarStyle.Literal or ScalarStyle.Folded or ScalarStyle.Any => JsonValue.Create(yaml.Value),
- _ => throw new ArgumentOutOfRangeException(nameof(yaml)),
+ ScalarStyle.Plain when decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => JsonValue.Create(d),
+ ScalarStyle.Plain when bool.TryParse(value, out var b) => JsonValue.Create(b),
+ ScalarStyle.Plain when value is not null && YamlNullRepresentations.Contains(value) => (JsonValue)JsonNullSentinel.JsonNull.DeepClone(),
+ ScalarStyle.Plain => JsonValue.Create(value ?? string.Empty),
+ ScalarStyle.SingleQuoted or ScalarStyle.DoubleQuoted or ScalarStyle.Literal or ScalarStyle.Folded or ScalarStyle.Any => JsonValue.Create(value ?? string.Empty),
+ _ => throw new ArgumentOutOfRangeException(nameof(style)),
};
}
+ private sealed class YamlConversionContext
+ {
+ private readonly YamlConversionBudget _budget;
+ private readonly Dictionary _completed = new(ReferenceEqualityComparer.Instance);
+ private readonly HashSet _active = new(ReferenceEqualityComparer.Instance);
+
+ public YamlConversionContext(YamlConversionBudget budget)
+ {
+ _budget = budget;
+ }
+
+ public MaterializedNode Convert(YamlNode yaml, uint depth)
+ {
+ try
+ {
+ RuntimeHelpers.EnsureSufficientExecutionStack();
+ }
+ catch (InsufficientExecutionStackException ex)
+ {
+ throw new OpenApiReaderException("The YAML node graph is too deeply nested to convert safely.", ex);
+ }
+
+ if (_active.Contains(yaml))
+ {
+ throw new OpenApiReaderException("The YAML node graph contains a cycle.");
+ }
+
+ if (_completed.TryGetValue(yaml, out var completed))
+ {
+ _budget.EnterAlias(depth, completed.NodeCount, completed.Height);
+ return new(completed.Node.DeepClone(), completed.NodeCount, completed.Height);
+ }
+
+ _budget.EnterNode(depth);
+ _active.Add(yaml);
+ try
+ {
+ var materialized = yaml switch
+ {
+ YamlMappingNode map => ConvertMapping(map, depth),
+ YamlSequenceNode sequence => ConvertSequence(sequence, depth),
+ YamlScalarNode scalar => new MaterializedNode(ToJsonValue(scalar.Value, scalar.Style), 1, 1),
+ _ => throw new NotSupportedException("This yaml isn't convertible to JSON")
+ };
+ _completed.Add(yaml, materialized);
+ return materialized;
+ }
+ finally
+ {
+ _active.Remove(yaml);
+ }
+ }
+
+ private MaterializedNode ConvertMapping(YamlMappingNode yaml, uint depth)
+ {
+ var node = new JsonObject();
+ uint nodeCount = 1;
+ uint maxChildHeight = 0;
+ foreach (var keyValuePair in yaml)
+ {
+ if (keyValuePair.Key is not YamlScalarNode scalarKey || scalarKey.Value is null)
+ {
+ throw new OpenApiReaderException("YAML mapping keys must be scalar values.");
+ }
+
+ if (node.ContainsKey(scalarKey.Value))
+ {
+ throw new OpenApiReaderException($"The YAML mapping contains the duplicate key '{scalarKey.Value}'.");
+ }
+
+ var child = Convert(keyValuePair.Value, depth + 1);
+ node.Add(scalarKey.Value, child.Node);
+ nodeCount = checked(nodeCount + child.NodeCount);
+ maxChildHeight = Math.Max(maxChildHeight, child.Height);
+ }
+
+ return new(node, nodeCount, maxChildHeight + 1);
+ }
+
+ private MaterializedNode ConvertSequence(YamlSequenceNode yaml, uint depth)
+ {
+ var node = new JsonArray();
+ uint nodeCount = 1;
+ uint maxChildHeight = 0;
+ foreach (var value in yaml)
+ {
+ var child = Convert(value, depth + 1);
+ node.Add(child.Node);
+ nodeCount = checked(nodeCount + child.NodeCount);
+ maxChildHeight = Math.Max(maxChildHeight, child.Height);
+ }
+
+ return new(node, nodeCount, maxChildHeight + 1);
+ }
+ }
+
+ private sealed class MaterializedNode
+ {
+ public MaterializedNode(JsonNode node, uint nodeCount, uint height)
+ {
+ Node = node;
+ NodeCount = nodeCount;
+ Height = height;
+ }
+
+ public JsonNode Node { get; }
+ public uint NodeCount { get; }
+
+ /// Number of levels in this subtree, where a scalar has height 1.
+ public uint Height { get; }
+ }
+
+ private sealed class ReferenceEqualityComparer : IEqualityComparer where T : class
+ {
+ public static ReferenceEqualityComparer Instance { get; } = new();
+
+ public bool Equals(T? x, T? y) => ReferenceEquals(x, y);
+
+ public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj);
+ }
+
private static bool NeedsQuoting(string value) =>
string.IsNullOrEmpty(value) ||
decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out _) ||
diff --git a/src/Microsoft.OpenApi.YamlReader/YamlJsonParser.cs b/src/Microsoft.OpenApi.YamlReader/YamlJsonParser.cs
new file mode 100644
index 000000000..c7a3aab90
--- /dev/null
+++ b/src/Microsoft.OpenApi.YamlReader/YamlJsonParser.cs
@@ -0,0 +1,291 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json.Nodes;
+using System.Threading;
+using SharpYaml;
+using SharpYaml.Events;
+
+namespace Microsoft.OpenApi.YamlReader;
+
+///
+/// Iteratively materializes the first YAML document from parser events under resource limits.
+///
+internal sealed class YamlJsonParser
+{
+ private const int LookAheadBufferCapacity = 8;
+
+ private readonly YamlConversionBudget _budget;
+ private readonly Dictionary _anchors = new(StringComparer.Ordinal);
+ private readonly HashSet _activeAnchors = new(StringComparer.Ordinal);
+ private readonly Stack _containers = new();
+ private readonly uint _maxScalarLength;
+ private readonly uint _maxDepth;
+ private JsonNode? _root;
+
+ public YamlJsonParser(OpenApiYamlReaderSettings settings)
+ {
+ _budget = new(settings.MaxDepth, settings.MaxNodeCount, settings.MaxAliasExpansionNodeCount);
+ _maxScalarLength = settings.MaxScalarLength;
+ _maxDepth = settings.MaxDepth;
+ }
+
+ public JsonNode Parse(TextReader input, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var cancellationReader = new CancellationTokenTextReader(input, cancellationToken);
+
+ // SharpYaml applies its own nesting limit, defaulting to 64. Passing the configured limit
+ // keeps the two enforcement points in agreement; leaving it unset would silently cap every
+ // reader at 64 regardless of MaxDepth, making values above the default a no-op.
+ var parser = new Parser(
+ new LookAheadBuffer(cancellationReader, LookAheadBufferCapacity),
+ (int)_maxDepth);
+ var documentStarted = false;
+
+ while (true)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ if (!parser.MoveNext())
+ {
+ break;
+ }
+
+ switch (parser.Current)
+ {
+ case StreamStart:
+ break;
+ case DocumentStart:
+ documentStarted = true;
+ break;
+ case MappingStart mappingStart:
+ StartContainer(new JsonObject(), mappingStart.Anchor);
+ break;
+ case SequenceStart sequenceStart:
+ StartContainer(new JsonArray(), sequenceStart.Anchor);
+ break;
+ case Scalar scalar:
+ AddScalar(scalar, cancellationToken);
+ break;
+ case AnchorAlias alias:
+ AddAlias(alias, cancellationToken);
+ break;
+ case MappingEnd:
+ case SequenceEnd:
+ EndContainer();
+ break;
+ case DocumentEnd:
+ return _root ?? throw new OpenApiReaderException("No content found in the YAML document.");
+ case StreamEnd:
+ if (documentStarted)
+ {
+ return _root ?? throw new OpenApiReaderException("No content found in the YAML document.");
+ }
+
+ throw new OpenApiReaderException("No documents found in the YAML stream.");
+ default:
+ throw new OpenApiReaderException(
+ $"Unsupported YAML parser event '{parser.Current?.GetType().Name ?? ""}'.");
+ }
+ }
+
+ throw new OpenApiReaderException("No documents found in the YAML stream.");
+ }
+
+ private void StartContainer(JsonNode container, string? anchor)
+ {
+ _budget.EnterNode((uint)_containers.Count);
+ RegisterActiveAnchor(anchor);
+ _containers.Push(new(container, anchor));
+ }
+
+ private void AddScalar(Scalar scalar, CancellationToken cancellationToken)
+ {
+ if (scalar.Value is { } value && value.Length > _maxScalarLength)
+ {
+ throw new OpenApiReaderException(
+ $"The YAML scalar exceeds the maximum supported length of {_maxScalarLength} characters.");
+ }
+
+ _budget.EnterNode((uint)_containers.Count);
+ cancellationToken.ThrowIfCancellationRequested();
+ var materialized = new MaterializedNode(
+ YamlConverter.ToJsonValue(scalar.Value, scalar.Style),
+ 1,
+ 1,
+ scalar.Value);
+
+ RegisterCompletedAnchor(scalar.Anchor, materialized);
+ AddNode(materialized);
+ }
+
+ private void AddAlias(AnchorAlias alias, CancellationToken cancellationToken)
+ {
+ if (_activeAnchors.Contains(alias.Value))
+ {
+ throw new OpenApiReaderException($"The YAML alias '*{alias.Value}' forms a cycle.");
+ }
+
+ if (!_anchors.TryGetValue(alias.Value, out var anchor))
+ {
+ throw new OpenApiReaderException($"The YAML alias '*{alias.Value}' refers to an unknown anchor.");
+ }
+
+ _budget.EnterAlias((uint)_containers.Count, anchor.NodeCount, anchor.Height);
+ cancellationToken.ThrowIfCancellationRequested();
+ AddNode(new(anchor.Node.DeepClone(), anchor.NodeCount, anchor.Height, anchor.MappingKey));
+ }
+
+ private void EndContainer()
+ {
+ if (_containers.Count == 0)
+ {
+ throw new OpenApiReaderException("The YAML document contains an unexpected container terminator.");
+ }
+
+ var frame = _containers.Pop();
+ if (frame.PendingKey is not null)
+ {
+ throw new OpenApiReaderException("The YAML mapping contains a key without a value.");
+ }
+
+ var materialized = new MaterializedNode(frame.Container, frame.NodeCount, frame.MaxChildHeight + 1, null);
+ if (frame.Anchor is not null)
+ {
+ _activeAnchors.Remove(frame.Anchor);
+ _anchors.Add(frame.Anchor, materialized);
+ }
+
+ AddNode(materialized);
+ }
+
+ private void AddNode(MaterializedNode materialized)
+ {
+ if (_containers.Count == 0)
+ {
+ if (_root is not null)
+ {
+ throw new OpenApiReaderException("The YAML document contains more than one root node.");
+ }
+
+ _root = materialized.Node;
+ return;
+ }
+
+ var frame = _containers.Peek();
+ switch (frame.Container)
+ {
+ case JsonArray array:
+ array.Add(materialized.Node);
+ frame.NodeCount = checked(frame.NodeCount + materialized.NodeCount);
+ frame.MaxChildHeight = Math.Max(frame.MaxChildHeight, materialized.Height);
+ break;
+ case JsonObject when frame.PendingKey is null:
+ frame.PendingKey = materialized.MappingKey
+ ?? throw new OpenApiReaderException("YAML mapping keys must be scalar values.");
+ break;
+ case JsonObject map:
+ if (map.ContainsKey(frame.PendingKey))
+ {
+ throw new OpenApiReaderException($"The YAML mapping contains the duplicate key '{frame.PendingKey}'.");
+ }
+
+ map.Add(frame.PendingKey, materialized.Node);
+ frame.PendingKey = null;
+ frame.NodeCount = checked(frame.NodeCount + materialized.NodeCount);
+ frame.MaxChildHeight = Math.Max(frame.MaxChildHeight, materialized.Height);
+ break;
+ }
+ }
+
+ private void RegisterActiveAnchor(string? anchor)
+ {
+ if (anchor is null || anchor.Length == 0)
+ {
+ return;
+ }
+
+ if (_anchors.ContainsKey(anchor) || !_activeAnchors.Add(anchor))
+ {
+ throw new OpenApiReaderException($"The YAML document contains the duplicate anchor '&{anchor}'.");
+ }
+ }
+
+ private void RegisterCompletedAnchor(string? anchor, MaterializedNode materialized)
+ {
+ if (anchor is null || anchor.Length == 0)
+ {
+ return;
+ }
+
+ if (_anchors.ContainsKey(anchor) || _activeAnchors.Contains(anchor))
+ {
+ throw new OpenApiReaderException($"The YAML document contains the duplicate anchor '&{anchor}'.");
+ }
+
+ _anchors.Add(anchor, materialized);
+ }
+
+ private sealed class ContainerFrame(JsonNode container, string? anchor)
+ {
+ public JsonNode Container { get; } = container;
+ public string? Anchor { get; } = anchor;
+ public string? PendingKey { get; set; }
+ public uint NodeCount { get; set; } = 1;
+
+ /// Height of the tallest child added so far; 0 while the container is empty.
+ public uint MaxChildHeight { get; set; }
+ }
+
+ private sealed class MaterializedNode
+ {
+ public MaterializedNode(JsonNode node, uint nodeCount, uint height, string? mappingKey)
+ {
+ Node = node;
+ NodeCount = nodeCount;
+ Height = height;
+ MappingKey = mappingKey;
+ }
+
+ public JsonNode Node { get; }
+ public uint NodeCount { get; }
+
+ /// Number of levels in this subtree, where a scalar has height 1.
+ public uint Height { get; }
+
+ public string? MappingKey { get; }
+ }
+
+ private sealed class CancellationTokenTextReader(TextReader innerReader, CancellationToken cancellationToken) : TextReader
+ {
+ public override int Peek()
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return innerReader.Peek();
+ }
+
+ public override int Read()
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return innerReader.Read();
+ }
+
+ public override int Read(char[] buffer, int index, int count)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return innerReader.Read(buffer, index, count);
+ }
+
+ public override string? ReadLine()
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return base.ReadLine();
+ }
+
+ public override string ReadToEnd()
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return base.ReadToEnd();
+ }
+ }
+}
diff --git a/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs b/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs
new file mode 100644
index 000000000..2523fdd6b
--- /dev/null
+++ b/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs
@@ -0,0 +1,102 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System;
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using Microsoft.OpenApi.Reader;
+
+namespace Microsoft.OpenApi
+{
+ ///
+ /// Enables System.Text.Json serialization and deserialization of
+ /// using the OpenAPI wire format rather than the default reflection-based output.
+ ///
+ ///
+ /// Register this converter via :
+ ///
+ /// var options = new JsonSerializerOptions();
+ /// options.Converters.Add(new OpenApiSchemaJsonConverter());
+ /// var json = JsonSerializer.Serialize(schema, options);
+ ///
+ ///
+ public sealed class OpenApiSchemaJsonConverter : JsonConverter
+ {
+ private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
+ private readonly OpenApiSpecVersion _version;
+
+ ///
+ /// Initializes a new instance of targeting OpenAPI 3.2.
+ ///
+ public OpenApiSchemaJsonConverter() : this(OpenApiSpecVersion.OpenApi3_2) { }
+
+ ///
+ /// Initializes a new instance of targeting the specified OpenAPI version.
+ ///
+ /// The OpenAPI specification version to use when serializing the schema.
+ public OpenApiSchemaJsonConverter(OpenApiSpecVersion version)
+ {
+ _version = version;
+ }
+
+ ///
+ ///
+ /// Deserializes a bare JSON Schema object into an using
+ /// to parse it as a schema fragment.
+ /// Only OpenAPI 3.x versions support JSON Schema; deserializing with
+ /// is not supported and will throw .
+ ///
+ public override OpenApiSchema? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (_version == OpenApiSpecVersion.OpenApi2_0)
+ throw new NotSupportedException("Deserializing OpenApiSchema is not supported for OpenAPI 2.0.");
+
+ var jsonNode = JsonNode.Parse(ref reader)
+ ?? throw new JsonException("Failed to parse the JSON input into a valid JsonNode.");
+ var jsonReader = new OpenApiJsonReader();
+ return jsonReader.ReadFragment(jsonNode, _version, new OpenApiDocument(), out _);
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, OpenApiSchema value, JsonSerializerOptions options)
+ {
+ Utils.CheckArgumentNull(writer);
+ Utils.CheckArgumentNull(value);
+
+ using var stream = new MemoryStream();
+ using (var textWriter = new StreamWriter(stream, Utf8NoBom, bufferSize: 1024, leaveOpen: true))
+ {
+ var openApiWriter = new OpenApiJsonWriter(textWriter);
+ SerializeSchema(value, openApiWriter);
+ textWriter.Flush();
+ }
+
+ writer.WriteRawValue(stream.ToArray(), skipInputValidation: true);
+ }
+
+ private void SerializeSchema(OpenApiSchema schema, OpenApiJsonWriter writer)
+ {
+ switch (_version)
+ {
+ case OpenApiSpecVersion.OpenApi3_2:
+ schema.SerializeAsV32(writer);
+ break;
+ case OpenApiSpecVersion.OpenApi3_1:
+ schema.SerializeAsV31(writer);
+ break;
+ case OpenApiSpecVersion.OpenApi3_0:
+ schema.SerializeAsV3(writer);
+ break;
+ case OpenApiSpecVersion.OpenApi2_0:
+ schema.SerializeAsV2(writer);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(_version), _version,
+ string.Format(SRResource.OpenApiSpecVersionNotSupported, _version));
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.OpenApi/Expressions/CompositeExpression.cs b/src/Microsoft.OpenApi/Expressions/CompositeExpression.cs
index cac554318..db2a97caf 100644
--- a/src/Microsoft.OpenApi/Expressions/CompositeExpression.cs
+++ b/src/Microsoft.OpenApi/Expressions/CompositeExpression.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@@ -10,10 +11,17 @@ namespace Microsoft.OpenApi
///
/// String literal with embedded expressions
///
- public class CompositeExpression : RuntimeExpression
+ public partial class CompositeExpression : RuntimeExpression
{
private readonly string template;
- private readonly Regex expressionPattern = new(@"{(?\$[^}]*)");
+ private const string ExpressionPattern = @"{(?\$[^}]*)";
+
+#if NET8_0_OR_GREATER
+ [GeneratedRegex(ExpressionPattern, RegexOptions.None, matchTimeoutMilliseconds: 100)]
+ private static partial Regex ExpressionRegex();
+#else
+ private static readonly Regex ExpressionRegex = new(ExpressionPattern, RegexOptions.None, TimeSpan.FromMilliseconds(100));
+#endif
///
/// Expressions embedded into string literal
@@ -24,12 +32,17 @@ public class CompositeExpression : RuntimeExpression
/// Create a composite expression from a string literal with an embedded expression
///
///
+ /// Extracting embedded expressions exceeds the regex match timeout.
public CompositeExpression(string expression)
{
template = expression;
// Extract subexpressions and convert to RuntimeExpressions
- var matches = expressionPattern.Matches(expression);
+#if NET8_0_OR_GREATER
+ var matches = ExpressionRegex().Matches(expression);
+#else
+ var matches = ExpressionRegex.Matches(expression);
+#endif
foreach (var item in matches.Cast())
{
diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
index 8d557b4d1..1ad3a5b8f 100644
--- a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
+++ b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
@@ -69,7 +69,12 @@ internal static string ToFirstIdentifier(this JsonSchemaType schemaType)
///
internal static string ToSingleIdentifier(this JsonSchemaType schemaType)
{
- return schemaType.ToIdentifiersInternal().Single();
+ if (allSchemaTypes.TryGetValue(schemaType, out var schemaTypeString))
+ {
+ return schemaTypeString;
+ }
+
+ throw new InvalidOperationException($"ToSingleIdentifier is called with unexpected value '{schemaType}'. Callers must ensure this is called with a valid single value JsonSchemaType.");
}
///
diff --git a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj
index a6ea923a6..8e1ebb555 100644
--- a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj
+++ b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj
@@ -24,13 +24,10 @@
true
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
+
+
+
+
@@ -54,7 +51,7 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs
index 5669a0bb3..586043d79 100644
--- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs
+++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace Microsoft.OpenApi;
@@ -42,6 +43,7 @@ public interface IOpenApiParameter : IOpenApiDescribedElement, IOpenApiReadOnlyE
/// If style is used, and if behavior is n/a (cannot be serialized),
/// the value of allowEmptyValue SHALL be ignored.
///
+ [Obsolete("Use of AllowEmptyValue is not recommended and it is likely to be removed in a later revision.")]
public bool AllowEmptyValue { get; }
///
diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs
index 6d43a087a..476b2a853 100644
--- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs
+++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs
@@ -243,6 +243,7 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
/// You must use the method to check whether Default was assigned a null value in the document.
/// Assign to use get null as a serialized value.
///
+ [Obsolete("Use Examples instead.")]
public JsonNode? Example { get; }
///
@@ -260,7 +261,7 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
///
/// Indicates whether unevaluated properties are allowed. When false, no unevaluated properties are permitted.
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-unevaluatedproperties
- /// Only serialized when false and UnevaluatedPropertiesSchema (from IOpenApiSchemaWithUnevaluatedProperties) is null.
+ /// Only serialized when false and UnevaluatedPropertiesSchema (from IOpenApiSchemaMissingProperties) is null.
///
///
/// NOTE: This property differs from the naming pattern of AdditionalPropertiesAllowed for binary compatibility reasons.
diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaMissingProperties.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaMissingProperties.cs
new file mode 100644
index 000000000..32337cd29
--- /dev/null
+++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaMissingProperties.cs
@@ -0,0 +1,114 @@
+using System.Collections.Generic;
+
+namespace Microsoft.OpenApi;
+
+///
+/// Compatibility interface for schema properties that cannot be added to
+/// in the current major version without a breaking change.
+/// This interface provides access to those properties in contexts where callers need a typed model surface.
+///
+///
+/// TODO: Remove this interface in the next major version and merge its content into IOpenApiSchema.
+///
+public interface IOpenApiSchemaMissingProperties
+{
+ ///
+ /// $anchor - identifies a plain-name location-independent fragment within the schema resource.
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-anchor
+ ///
+ public string? Anchor { get; }
+
+ ///
+ /// Indicates whether unevaluated properties are allowed. When false, no unevaluated properties are permitted.
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-unevaluatedproperties
+ /// Only serialized when false and is null.
+ ///
+ ///
+ /// NOTE: This property differs from the naming pattern of AdditionalPropertiesAllowed for binary compatibility reasons.
+ /// In the next major version, this will be renamed to UnevaluatedPropertiesAllowed.
+ /// TODO: Rename to UnevaluatedPropertiesAllowed in the next major version.
+ ///
+ public bool UnevaluatedProperties { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-unevaluatedproperties
+ /// This is a schema that unevaluated properties must validate against.
+ /// When serialized, this takes precedence over the boolean property.
+ ///
+ ///
+ /// NOTE: This property differs from the naming pattern of AdditionalProperties/AdditionalPropertiesAllowed
+ /// for binary compatibility reasons. In the next major version:
+ /// - This property will be renamed to UnevaluatedProperties
+ /// - The current boolean UnevaluatedProperties property will be renamed to UnevaluatedPropertiesAllowed
+ ///
+ /// TODO: Rename this property to UnevaluatedProperties in the next major version.
+ ///
+ public IOpenApiSchema? UnevaluatedPropertiesSchema { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation#name-contentencoding
+ /// contentEncoding - identifies the encoding of string content.
+ ///
+ public string? ContentEncoding { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation#name-contentmediatype
+ /// contentMediaType - identifies the media type of string content.
+ ///
+ public string? ContentMediaType { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation#name-contentschema
+ /// contentSchema - provides a schema that describes the decoded string content.
+ ///
+ public IOpenApiSchema? ContentSchema { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-propertynames
+ /// propertyNames - provides a schema that validates property names.
+ ///
+ public IOpenApiSchema? PropertyNames { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-dependentschemas
+ /// dependentSchemas - maps property names to schemas that are applied when that property is present.
+ ///
+ public IDictionary? DependentSchemas { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-if
+ /// if - applies a conditional schema that determines whether or should be evaluated.
+ ///
+ public IOpenApiSchema? If { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-then
+ /// then - applies when evaluates successfully.
+ ///
+ public IOpenApiSchema? Then { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-else
+ /// else - applies when does not evaluate successfully.
+ ///
+ public IOpenApiSchema? Else { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-contains
+ /// An array instance is valid against "contains" if at least one of its elements is valid against this schema.
+ /// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema.
+ ///
+ IOpenApiSchema? Contains { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
+ /// The number of elements matching the "contains" schema MUST be less than or equal to this value.
+ ///
+ uint? MaxContains { get; }
+
+ ///
+ /// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
+ /// The number of elements matching the "contains" schema MUST be greater than or equal to this value.
+ ///
+ uint? MinContains { get; }
+}
diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaWithUnevaluatedProperties.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaWithUnevaluatedProperties.cs
index 3379a0837..6fe7e9f9a 100644
--- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaWithUnevaluatedProperties.cs
+++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaWithUnevaluatedProperties.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace Microsoft.OpenApi;
///
@@ -13,6 +15,7 @@ namespace Microsoft.OpenApi;
///
/// TODO: Remove this interface in the next major version and merge its content into IOpenApiSchema.
///
+[Obsolete("Use IOpenApiSchemaMissingProperties instead.")]
public interface IOpenApiSchemaWithUnevaluatedProperties
{
///
diff --git a/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs b/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs
index fff77e4cc..c495ddbea 100644
--- a/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs
+++ b/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;
@@ -8,6 +8,8 @@
namespace Microsoft.OpenApi;
+#pragma warning disable CS0618
+
///
/// Schema reference information that includes metadata annotations from JSON Schema 2020-12.
/// This class extends OpenApiReference to provide schema-specific metadata override capabilities.
@@ -58,6 +60,307 @@ public class JsonSchemaReference : OpenApiReferenceWithDescription
///
public IDictionary? Extensions { get; set; }
+ ///
+ /// A $id which by default SHOULD override that of the referenced component.
+ /// Named SchemaId to avoid collision with the inherited reference identifier (BaseOpenApiReference.Id).
+ ///
+ public string? SchemaId { get; set; }
+
+ ///
+ /// The $schema dialect URI which by default SHOULD override that of the referenced component.
+ ///
+ public Uri? Schema { get; set; }
+
+ ///
+ /// A $comment which by default SHOULD override that of the referenced component.
+ ///
+ public string? Comment { get; set; }
+
+ ///
+ /// The $vocabulary which by default SHOULD override that of the referenced component.
+ ///
+ public IDictionary? Vocabulary { get; set; }
+
+ ///
+ /// The $dynamicRef which by default SHOULD override that of the referenced component.
+ ///
+ public string? DynamicRef { get; set; }
+
+ ///
+ /// The $dynamicAnchor which by default SHOULD override that of the referenced component.
+ ///
+ public string? DynamicAnchor { get; set; }
+
+ ///
+ /// The $defs which by default SHOULD override that of the referenced component.
+ ///
+ public IDictionary? Definitions { get; set; }
+
+ ///
+ /// The $anchor which by default SHOULD override that of the referenced component.
+ ///
+ public string? Anchor { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public string? ExclusiveMaximum { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public string? ExclusiveMinimum { get; set; }
+
+ ///
+ /// Schema type override. Named SchemaType to avoid collision with .
+ ///
+ public JsonSchemaType? SchemaType { get; set; }
+
+ internal bool WasConstExplicitlySet { get; private set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public string? Const
+ {
+ get => field;
+ set
+ {
+ // TODO: In the next major release, Const should be made a JsonNode.
+ // See https://github.com/microsoft/OpenAPI.NET/issues/2935 for more information.
+ WasConstExplicitlySet = true;
+ field = value;
+ }
+ }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public string? Format { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public string? Maximum { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public string? Minimum { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public int? MaxLength { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public int? MinLength { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public string? Pattern { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public decimal? MultipleOf { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IList? AllOf { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IList? OneOf { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IList? AnyOf { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IOpenApiSchema? Not { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public ISet? Required { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IOpenApiSchema? Items { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public int? MaxItems { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public int? MinItems { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public bool? UniqueItems { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IOpenApiSchema? Contains { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public uint? MaxContains { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public uint? MinContains { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IDictionary? Properties { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IDictionary? PatternProperties { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public int? MaxProperties { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public int? MinProperties { get; set; }
+
+ ///
+ /// Indicates if the schema can contain properties other than those defined by the properties map.
+ /// Follow JSON Schema definition.
+ ///
+ public bool? AdditionalPropertiesAllowed { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IOpenApiSchema? AdditionalProperties { get; set; }
+
+ ///
+ /// Adds support for polymorphism.
+ /// Follow OpenAPI definition.
+ ///
+ public OpenApiDiscriminator? Discriminator { get; set; }
+
+ ///
+ /// A free-form property to include an example of an instance for this schema.
+ /// Follow OpenAPI Schema Object definition.
+ ///
+ [Obsolete("Use Examples instead.")]
+ public JsonNode? Example { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IList? Enum { get; set; }
+
+ ///
+ /// Indicates whether unevaluated properties are allowed.
+ /// Follow JSON Schema definition.
+ ///
+ public bool? UnevaluatedProperties { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IOpenApiSchema? UnevaluatedPropertiesSchema { get; set; }
+
+ ///
+ /// Additional external documentation for this schema.
+ /// Follow OpenAPI definition.
+ ///
+ public OpenApiExternalDocs? ExternalDocs { get; set; }
+
+ ///
+ /// This MAY be used only on properties schemas.
+ /// Follow OpenAPI definition.
+ ///
+ public OpenApiXml? Xml { get; set; }
+
+ ///
+ /// This object stores any unrecognized keywords found in the schema.
+ ///
+ public IDictionary? UnrecognizedKeywords { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IDictionary>? DependentRequired { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public string? ContentEncoding { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public string? ContentMediaType { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IOpenApiSchema? ContentSchema { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IOpenApiSchema? PropertyNames { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IDictionary? DependentSchemas { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IOpenApiSchema? If { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IOpenApiSchema? Then { get; set; }
+
+ ///
+ /// Follow JSON Schema definition.
+ ///
+ public IOpenApiSchema? Else { get; set; }
+
+ ///
+ /// Indicates whether this reference represents a bare $dynamicRef (no $ref to a component).
+ /// When true, serialization emits $dynamicRef instead of $ref, and Target resolution
+ /// uses the $dynamicAnchor index rather than the $ref URI lookup.
+ /// Computed from whether $dynamicRef is set and ReferenceV3 does not point to a component path.
+ ///
+ internal bool IsDynamicRefOnly
+ => !string.IsNullOrEmpty(DynamicRef)
+ && ReferenceV3 is string refV3
+ && !refV3.StartsWith("#/components/", StringComparison.OrdinalIgnoreCase);
+
///
/// Parameterless constructor
///
@@ -76,24 +379,188 @@ public JsonSchemaReference(JsonSchemaReference reference) : base(reference)
WriteOnly = reference.WriteOnly;
Examples = reference.Examples;
Extensions = reference.Extensions != null ? new Dictionary(reference.Extensions) : null;
+ SchemaId = reference.SchemaId;
+ Schema = reference.Schema;
+ Comment = reference.Comment;
+ Vocabulary = reference.Vocabulary != null ? new Dictionary(reference.Vocabulary) : null;
+ DynamicRef = reference.DynamicRef;
+ DynamicAnchor = reference.DynamicAnchor;
+ Definitions = reference.Definitions != null ? new Dictionary(reference.Definitions) : null;
+ Anchor = reference.Anchor;
+ ExclusiveMaximum = reference.ExclusiveMaximum;
+ ExclusiveMinimum = reference.ExclusiveMinimum;
+ SchemaType = reference.SchemaType;
+ Const = reference.Const;
+ WasConstExplicitlySet = reference.WasConstExplicitlySet;
+ Format = reference.Format;
+ Maximum = reference.Maximum;
+ Minimum = reference.Minimum;
+ MaxLength = reference.MaxLength;
+ MinLength = reference.MinLength;
+ Pattern = reference.Pattern;
+ MultipleOf = reference.MultipleOf;
+ AllOf = reference.AllOf != null ? [.. reference.AllOf] : null;
+ OneOf = reference.OneOf != null ? [.. reference.OneOf] : null;
+ AnyOf = reference.AnyOf != null ? [.. reference.AnyOf] : null;
+ Not = reference.Not;
+ Required = reference.Required != null ? new HashSet(reference.Required) : null;
+ Items = reference.Items;
+ MaxItems = reference.MaxItems;
+ MinItems = reference.MinItems;
+ UniqueItems = reference.UniqueItems;
+ Contains = reference.Contains;
+ MaxContains = reference.MaxContains;
+ MinContains = reference.MinContains;
+ Properties = reference.Properties != null ? new Dictionary(reference.Properties) : null;
+ PatternProperties = reference.PatternProperties != null ? new Dictionary(reference.PatternProperties) : null;
+ MaxProperties = reference.MaxProperties;
+ MinProperties = reference.MinProperties;
+ AdditionalPropertiesAllowed = reference.AdditionalPropertiesAllowed;
+ AdditionalProperties = reference.AdditionalProperties;
+ Discriminator = reference.Discriminator;
+ Example = reference.Example;
+ Enum = reference.Enum != null ? [.. reference.Enum] : null;
+ UnevaluatedProperties = reference.UnevaluatedProperties;
+ UnevaluatedPropertiesSchema = reference.UnevaluatedPropertiesSchema;
+ ExternalDocs = reference.ExternalDocs;
+ Xml = reference.Xml;
+ UnrecognizedKeywords = reference.UnrecognizedKeywords != null ? new Dictionary(reference.UnrecognizedKeywords) : null;
+ DependentRequired = reference.DependentRequired != null ? new Dictionary>(reference.DependentRequired) : null;
+ ContentEncoding = reference.ContentEncoding;
+ ContentMediaType = reference.ContentMediaType;
+ ContentSchema = reference.ContentSchema;
+ PropertyNames = reference.PropertyNames;
+ DependentSchemas = reference.DependentSchemas != null ? new Dictionary(reference.DependentSchemas) : null;
+ If = reference.If;
+ Then = reference.Then;
+ Else = reference.Else;
+ }
+
+ ///
+ public override void SerializeAsV31(IOpenApiWriter writer)
+ {
+ if (IsDynamicRefOnly)
+ {
+ writer.WriteStartObject();
+ SerializeAdditionalV3XProperties(writer, (w, e) => e.SerializeAsV31(w), base.SerializeAdditionalV31Properties);
+ writer.WriteEndObject();
+ }
+ else
+ {
+ base.SerializeAsV31(writer);
+ }
+ }
+ ///
+ public override void SerializeAsV32(IOpenApiWriter writer)
+ {
+ if (IsDynamicRefOnly)
+ {
+ writer.WriteStartObject();
+ SerializeAdditionalV3XProperties(writer, (w, e) => e.SerializeAsV32(w), base.SerializeAdditionalV32Properties);
+ writer.WriteEndObject();
+ }
+ else
+ {
+ base.SerializeAsV32(writer);
+ }
}
///
protected override void SerializeAdditionalV31Properties(IOpenApiWriter writer)
{
- SerializeAdditionalV3XProperties(writer, base.SerializeAdditionalV31Properties);
+ SerializeAdditionalV3XProperties(writer, (w, e) => e.SerializeAsV31(w), base.SerializeAdditionalV31Properties);
}
///
protected override void SerializeAdditionalV32Properties(IOpenApiWriter writer)
{
- SerializeAdditionalV3XProperties(writer, base.SerializeAdditionalV32Properties);
+ SerializeAdditionalV3XProperties(writer, (w, e) => e.SerializeAsV32(w), base.SerializeAdditionalV32Properties);
}
- private void SerializeAdditionalV3XProperties(IOpenApiWriter writer, Action baseSerializer)
+
+ private void SerializeAdditionalV3XProperties(IOpenApiWriter writer, Action serializeCallback, Action baseSerializer)
{
if (Type != ReferenceType.Schema) throw new InvalidOperationException(
$"JsonSchemaReference can only be serialized for ReferenceType.Schema, but was {Type}.");
baseSerializer(writer);
+
+ // JSON Schema 2020-12 keyword siblings (preserved per OAS 3.1+ / JSON Schema 2020-12 semantics)
+ writer.WriteProperty(OpenApiConstants.Id, SchemaId);
+ writer.WriteProperty(OpenApiConstants.DollarSchema, Schema?.ToString());
+ writer.WriteProperty(OpenApiConstants.Comment, Comment);
+ writer.WriteOptionalMap(OpenApiConstants.Vocabulary, Vocabulary, (w, s) => w.WriteValue(s));
+ writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, serializeCallback);
+ writer.WriteProperty(OpenApiConstants.Anchor, Anchor);
+ writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
+ writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);
+
+ if (WasConstExplicitlySet)
+ {
+ writer.WriteRequiredProperty(OpenApiConstants.Const, Const);
+ }
+
+ WriteSchemaType(writer, OpenApiConstants.Type, SchemaType, allowMultipleTypes: true);
+ writer.WriteProperty(OpenApiConstants.Format, Format);
+ writer.WriteProperty(OpenApiConstants.MultipleOf, MultipleOf);
+ WriteRawProperty(writer, OpenApiConstants.Maximum, Maximum);
+ WriteRawProperty(writer, OpenApiConstants.ExclusiveMaximum, ExclusiveMaximum);
+ WriteRawProperty(writer, OpenApiConstants.Minimum, Minimum);
+ WriteRawProperty(writer, OpenApiConstants.ExclusiveMinimum, ExclusiveMinimum);
+ writer.WriteProperty(OpenApiConstants.MaxLength, MaxLength);
+ writer.WriteProperty(OpenApiConstants.MinLength, MinLength);
+ writer.WriteProperty(OpenApiConstants.Pattern, Pattern);
+ writer.WriteProperty(OpenApiConstants.MaxItems, MaxItems);
+ writer.WriteProperty(OpenApiConstants.MinItems, MinItems);
+ writer.WriteProperty(OpenApiConstants.UniqueItems, UniqueItems);
+ writer.WriteProperty(OpenApiConstants.MaxProperties, MaxProperties);
+ writer.WriteProperty(OpenApiConstants.MinProperties, MinProperties);
+ writer.WriteOptionalCollection(OpenApiConstants.Required, Required, (w, s) =>
+ {
+ if (!string.IsNullOrEmpty(s))
+ {
+ w.WriteValue(s!);
+ }
+ });
+ writer.WriteOptionalCollection(OpenApiConstants.Enum, Enum, (w, e) => w.WriteAny(e));
+ writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, serializeCallback);
+ writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, serializeCallback);
+ writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, serializeCallback);
+ writer.WriteOptionalObject(OpenApiConstants.Not, Not, serializeCallback);
+ writer.WriteOptionalObject(OpenApiConstants.Items, Items, serializeCallback);
+ writer.WriteOptionalMap(OpenApiConstants.Properties, Properties, serializeCallback);
+ writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, serializeCallback);
+ if (AdditionalProperties is not null)
+ {
+ writer.WriteOptionalObject(OpenApiConstants.AdditionalProperties, AdditionalProperties, serializeCallback);
+ }
+ else if (AdditionalPropertiesAllowed.HasValue)
+ {
+ writer.WriteProperty(OpenApiConstants.AdditionalProperties, AdditionalPropertiesAllowed.Value);
+ }
+ writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, serializeCallback);
+ writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e));
+ if (UnevaluatedPropertiesSchema is not null)
+ {
+ writer.WriteOptionalObject(OpenApiConstants.UnevaluatedProperties, UnevaluatedPropertiesSchema, serializeCallback);
+ }
+ else if (UnevaluatedProperties.HasValue)
+ {
+ writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties.Value);
+ }
+ writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, serializeCallback);
+ writer.WriteOptionalObject(OpenApiConstants.Xml, Xml, serializeCallback);
+ writer.WriteOptionalMap(OpenApiConstants.DependentRequired, DependentRequired, (w, s) => w.WriteValue(s));
+ writer.WriteOptionalObject(OpenApiConstants.Contains, Contains, serializeCallback);
+ writer.WriteProperty(OpenApiConstants.MaxContains, MaxContains);
+ writer.WriteProperty(OpenApiConstants.MinContains, MinContains);
+ writer.WriteProperty(OpenApiConstants.ContentEncoding, ContentEncoding);
+ writer.WriteProperty(OpenApiConstants.ContentMediaType, ContentMediaType);
+ writer.WriteOptionalObject(OpenApiConstants.ContentSchema, ContentSchema, serializeCallback);
+ writer.WriteOptionalObject(OpenApiConstants.PropertyNames, PropertyNames, serializeCallback);
+ writer.WriteOptionalMap(OpenApiConstants.DependentSchemas, DependentSchemas, serializeCallback);
+ writer.WriteOptionalObject(OpenApiConstants.If, If, serializeCallback);
+ writer.WriteOptionalObject(OpenApiConstants.Then, Then, serializeCallback);
+ writer.WriteOptionalObject(OpenApiConstants.Else, Else, serializeCallback);
+
// Additional schema metadata annotations in 3.1
writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d));
writer.WriteProperty(OpenApiConstants.Title, Title);
@@ -116,53 +583,141 @@ private void SerializeAdditionalV3XProperties(IOpenApiWriter writer, Action
- protected override void SetAdditional31MetadataFromMapNode(JsonObject jsonObject)
+ private static void WriteRawProperty(IOpenApiWriter writer, string name, string? value)
{
- base.SetAdditional31MetadataFromMapNode(jsonObject);
-
- var title = GetPropertyValueFromNode(jsonObject, OpenApiConstants.Title);
- if (!string.IsNullOrEmpty(title))
+ if (!string.IsNullOrEmpty(value))
{
- Title = title;
+ writer.WritePropertyName(name);
+ writer.WriteRaw(value!);
}
+ }
- // Boolean properties
- if (jsonObject.TryGetPropertyValue(OpenApiConstants.Deprecated, out var deprecatedNode) && deprecatedNode is JsonValue deprecatedValue && deprecatedValue.TryGetValue(out var deprecated))
+ private static void WriteSchemaType(IOpenApiWriter writer, string name, JsonSchemaType? schemaType, bool allowMultipleTypes)
+ {
+ if (!schemaType.HasValue)
{
- Deprecated = deprecated;
+ return;
}
- if (jsonObject.TryGetPropertyValue(OpenApiConstants.ReadOnly, out var readOnlyNode) && readOnlyNode is JsonValue readOnlyValue && readOnlyValue.TryGetValue(out var readOnly))
+ var values = schemaType.Value.ToIdentifiers();
+ if (values is null || values.Length == 0)
{
- ReadOnly = readOnly;
+ return;
}
- if (jsonObject.TryGetPropertyValue(OpenApiConstants.WriteOnly, out var writeOnlyNode) && writeOnlyNode is JsonValue writeOnlyValue && writeOnlyValue.TryGetValue(out var writeOnly))
+ if (allowMultipleTypes && values.Length > 1)
{
- WriteOnly = writeOnly;
+ writer.WriteOptionalCollection(name, values, (w, s) =>
+ {
+ if (!string.IsNullOrEmpty(s))
+ {
+ w.WriteValue(s!);
+ }
+ });
}
-
- // Default value
- if (jsonObject.TryGetPropertyValue(OpenApiConstants.Default, out var defaultNode))
+ else
{
- Default = defaultNode;
+ writer.WriteProperty(name, values[0]);
}
+ }
+
+ ///
+ [Obsolete("Use ApplySchemaMetadata instead.")]
+#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member
+ protected override void SetAdditional31MetadataFromMapNode(JsonObject jsonObject)
+#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member
+ {
+ //TODO remove this method in next major release
+ // no-op: we're using ApplySchemaMetadata
+ }
- // Examples
- if (jsonObject.TryGetPropertyValue(OpenApiConstants.Examples, out var examplesNode) && examplesNode is JsonArray examplesArray)
+ internal void ApplySchemaMetadata(OpenApiSchema schema, JsonObject jsonObject)
+ {
+ Title = schema.Title;
+ Description = schema.Description;
+ Default = schema.Default;
+ if (jsonObject.ContainsKey(OpenApiConstants.Deprecated))
{
- Examples = examplesArray.OfType().ToList();
+ Deprecated = schema.Deprecated;
}
-
- // Extensions (properties starting with "x-")
- foreach (var property in jsonObject
- .Where(static p => p.Key.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase)
- && p.Value is not null))
+ if (jsonObject.ContainsKey(OpenApiConstants.ReadOnly))
+ {
+ ReadOnly = schema.ReadOnly;
+ }
+ if (jsonObject.ContainsKey(OpenApiConstants.WriteOnly))
+ {
+ WriteOnly = schema.WriteOnly;
+ }
+ Examples = schema.Examples;
+ Extensions = schema.Extensions;
+ SchemaId = schema.Id;
+ Schema = schema.Schema;
+ Comment = schema.Comment;
+ if (schema.Vocabulary is { Count: > 0 })
+ {
+ Vocabulary = schema.Vocabulary;
+ }
+ DynamicRef = schema.DynamicRef;
+ DynamicAnchor = schema.DynamicAnchor;
+ if (schema.Definitions is { Count: > 0 })
+ {
+ Definitions = schema.Definitions;
+ }
+ Anchor = schema.Anchor;
+ ExclusiveMaximum = schema.ExclusiveMaximum;
+ ExclusiveMinimum = schema.ExclusiveMinimum;
+ SchemaType = schema.Type;
+ Const = schema.Const;
+ WasConstExplicitlySet = schema.WasConstExplicitlySet;
+ Format = schema.Format;
+ Maximum = schema.Maximum;
+ Minimum = schema.Minimum;
+ MaxLength = schema.MaxLength;
+ MinLength = schema.MinLength;
+ Pattern = schema.Pattern;
+ MultipleOf = schema.MultipleOf;
+ AllOf = schema.AllOf;
+ OneOf = schema.OneOf;
+ AnyOf = schema.AnyOf;
+ Not = schema.Not;
+ Required = schema.Required;
+ Items = schema.Items;
+ MaxItems = schema.MaxItems;
+ MinItems = schema.MinItems;
+ UniqueItems = schema.UniqueItems;
+ Contains = schema.Contains;
+ MaxContains = schema.MaxContains;
+ MinContains = schema.MinContains;
+ Properties = schema.Properties;
+ PatternProperties = schema.PatternProperties;
+ MaxProperties = schema.MaxProperties;
+ MinProperties = schema.MinProperties;
+ if (jsonObject.TryGetPropertyValue(OpenApiConstants.AdditionalProperties, out var additionalPropertiesNode) &&
+ additionalPropertiesNode is JsonValue)
+ {
+ AdditionalPropertiesAllowed = schema.AdditionalPropertiesAllowed;
+ }
+ AdditionalProperties = schema.AdditionalProperties;
+ Discriminator = schema.Discriminator;
+ Example = schema.Example;
+ Enum = schema.Enum;
+ if (jsonObject.TryGetPropertyValue(OpenApiConstants.UnevaluatedProperties, out var unevaluatedPropertiesNode) &&
+ unevaluatedPropertiesNode is JsonValue)
{
- var extensionValue = property.Value!;
- Extensions ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
- Extensions[property.Key] = new JsonNodeExtension(extensionValue.DeepClone());
+ UnevaluatedProperties = schema.UnevaluatedProperties;
}
+ UnevaluatedPropertiesSchema = schema.UnevaluatedPropertiesSchema;
+ ExternalDocs = schema.ExternalDocs;
+ Xml = schema.Xml;
+ UnrecognizedKeywords = schema.UnrecognizedKeywords;
+ DependentRequired = schema.DependentRequired;
+ ContentEncoding = schema.ContentEncoding;
+ ContentMediaType = schema.ContentMediaType;
+ ContentSchema = schema.ContentSchema;
+ PropertyNames = schema.PropertyNames;
+ DependentSchemas = schema.DependentSchemas;
+ If = schema.If;
+ Then = schema.Then;
+ Else = schema.Else;
}
}
diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
index a54758002..706af35fc 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs
@@ -90,6 +90,11 @@ public static class OpenApiConstants
///
public const string Vocabulary = "$vocabulary";
+ ///
+ /// Field: Anchor
+ ///
+ public const string Anchor = "$anchor";
+
///
/// Field: DynamicRef
///
@@ -230,6 +235,11 @@ public static class OpenApiConstants
///
public const string Identifier = "identifier";
+ ///
+ /// Field: x-oai-license-identifier
+ ///
+ public const string OaiLicenseIdentifier = "x-oai-license-identifier";
+
///
/// Field: Namespace
///
@@ -485,6 +495,21 @@ public static class OpenApiConstants
///
public const string UniqueItems = "uniqueItems";
+ ///
+ /// Field: Contains
+ ///
+ public const string Contains = "contains";
+
+ ///
+ /// Field: MaxContains
+ ///
+ public const string MaxContains = "maxContains";
+
+ ///
+ /// Field: MinContains
+ ///
+ public const string MinContains = "minContains";
+
///
/// Field: MaxProperties
///
@@ -535,11 +560,51 @@ public static class OpenApiConstants
///
public const string PatternProperties = "patternProperties";
+ ///
+ /// Field: PropertyNames
+ ///
+ public const string PropertyNames = "propertyNames";
+
///
/// Extension: x-jsonschema-patternProperties
///
public const string PatternPropertiesExtension = "x-jsonschema-patternProperties";
+ ///
+ /// Field: DependentSchemas
+ ///
+ public const string DependentSchemas = "dependentSchemas";
+
+ ///
+ /// Field: If
+ ///
+ public const string If = "if";
+
+ ///
+ /// Field: Then
+ ///
+ public const string Then = "then";
+
+ ///
+ /// Field: Else
+ ///
+ public const string Else = "else";
+
+ ///
+ /// Field: ContentEncoding
+ ///
+ public const string ContentEncoding = "contentEncoding";
+
+ ///
+ /// Field: ContentMediaType
+ ///
+ public const string ContentMediaType = "contentMediaType";
+
+ ///
+ /// Field: ContentSchema
+ ///
+ public const string ContentSchema = "contentSchema";
+
///
/// Field: AdditionalProperties
///
@@ -710,6 +775,11 @@ public static class OpenApiConstants
///
public const string OAuth2MetadataUrl = "oauth2MetadataUrl";
+ ///
+ /// Extension: x-oai-oauth2-metadata-url
+ ///
+ internal const string OAuth2MetadataUrlExtension = "x-oai-oauth2-metadata-url";
+
///
/// Field: OpenIdConnectUrl
///
@@ -745,6 +815,11 @@ public static class OpenApiConstants
///
public const string ExamplesExtension = "x-examples";
+ ///
+ /// Extension: x-jsonschema-examples
+ ///
+ public const string JsonSchemaExamplesExtension = "x-jsonschema-examples";
+
///
/// Field: version3_0_0
///
@@ -790,6 +865,66 @@ public static class OpenApiConstants
///
public const string DependentRequired = "dependentRequired";
+ ///
+ /// Extension: x-jsonschema-$anchor
+ ///
+ public const string AnchorExtension = "x-jsonschema-$anchor";
+
+ ///
+ /// Extension: x-jsonschema-propertyNames
+ ///
+ public const string PropertyNamesExtension = "x-jsonschema-propertyNames";
+
+ ///
+ /// Extension: x-jsonschema-dependentSchemas
+ ///
+ public const string DependentSchemasExtension = "x-jsonschema-dependentSchemas";
+
+ ///
+ /// Extension: x-jsonschema-if
+ ///
+ public const string IfExtension = "x-jsonschema-if";
+
+ ///
+ /// Extension: x-jsonschema-then
+ ///
+ public const string ThenExtension = "x-jsonschema-then";
+
+ ///
+ /// Extension: x-jsonschema-else
+ ///
+ public const string ElseExtension = "x-jsonschema-else";
+
+ ///
+ /// Extension: x-jsonschema-contentEncoding
+ ///
+ public const string ContentEncodingExtension = "x-jsonschema-contentEncoding";
+
+ ///
+ /// Extension: x-jsonschema-contentMediaType
+ ///
+ public const string ContentMediaTypeExtension = "x-jsonschema-contentMediaType";
+
+ ///
+ /// Extension: x-jsonschema-contentSchema
+ ///
+ public const string ContentSchemaExtension = "x-jsonschema-contentSchema";
+
+ ///
+ /// Extension: x-jsonschema-contains
+ ///
+ public const string ContainsExtension = "x-jsonschema-contains";
+
+ ///
+ /// Extension: x-jsonschema-maxContains
+ ///
+ public const string MaxContainsExtension = "x-jsonschema-maxContains";
+
+ ///
+ /// Extension: x-jsonschema-minContains
+ ///
+ public const string MinContainsExtension = "x-jsonschema-minContains";
+
#region V2.0
///
diff --git a/src/Microsoft.OpenApi/Models/OpenApiDiscriminator.cs b/src/Microsoft.OpenApi/Models/OpenApiDiscriminator.cs
index 69e15f9a4..ed7a5f4d3 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiDiscriminator.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiDiscriminator.cs
@@ -58,7 +58,7 @@ public void SerializeAsV32(IOpenApiWriter writer)
if (DefaultMapping != null)
{
writer.WritePropertyName("defaultMapping");
- DefaultMapping.SerializeAsV32(writer);
+ WriteMappingReference(writer, DefaultMapping);
}
// extensions
@@ -79,7 +79,7 @@ public void SerializeAsV31(IOpenApiWriter writer)
if (DefaultMapping != null)
{
writer.WritePropertyName("x-oas-default-mapping");
- DefaultMapping.SerializeAsV31(writer);
+ WriteMappingReference(writer, DefaultMapping);
}
// extensions
@@ -108,13 +108,16 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
writer.WriteProperty(OpenApiConstants.PropertyName, PropertyName);
// mapping
- writer.WriteOptionalMap(OpenApiConstants.Mapping, Mapping, (w, s) =>
+ writer.WriteOptionalMap(OpenApiConstants.Mapping, Mapping, WriteMappingReference);
+ }
+
+ private static void WriteMappingReference(IOpenApiWriter writer, OpenApiSchemaReference schemaReference)
+ {
+ var reference = schemaReference.Reference.ReferenceV3;
+ if (!string.IsNullOrEmpty(reference))
{
- if (!string.IsNullOrEmpty(s.Reference.ReferenceV3) && s.Reference.ReferenceV3 is not null)
- {
- w.WriteValue(s.Reference.ReferenceV3);
- }
- });
+ writer.WriteValue(reference!);
+ }
}
///
diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
index 96973a701..03086600d 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
@@ -89,6 +89,7 @@ public ISet? Tags
{
if (value is null)
{
+ _tags = null;
return;
}
_tags = value switch
@@ -581,8 +582,11 @@ public async Task GetHashCodeAsync(CancellationToken cancellationToken =
using var streamWriter = new StreamWriter(cryptoStream);
await WriteDocumentAsync(streamWriter, cancellationToken).ConfigureAwait(false);
-
+#if NET5_0_OR_GREATER
+ await cryptoStream.FlushFinalBlockAsync(cancellationToken).ConfigureAwait(false);
+#else
cryptoStream.FlushFinalBlock();
+#endif
var hash = sha.Hash;
#endif
@@ -635,7 +639,11 @@ private static string ConvertByteArrayToString(byte[] hash)
string relativePath;
var referenceV3 = !string.IsNullOrEmpty(reference.ReferenceV3) ? reference.ReferenceV3! : string.Empty;
- if (!string.IsNullOrEmpty(referenceV3) && IsSubComponent(referenceV3))
+ if (reference.Type is ReferenceType.Schema && useExternal && TryGetPlainNameFragment(referenceV3, out var plainNameFragment))
+ {
+ relativePath = plainNameFragment;
+ }
+ else if (!string.IsNullOrEmpty(referenceV3) && IsSubComponent(referenceV3))
{
// Enables setting the complete JSON path for nested subschemas e.g. #/components/schemas/person/properties/address
if (useExternal)
@@ -698,6 +706,25 @@ private static bool IsSubComponent(string reference)
return false;
}
+ private static bool TryGetPlainNameFragment(string reference, out string fragment)
+ {
+ fragment = string.Empty;
+ var parts = reference.Split('#');
+ if (parts.Length == 2 &&
+ !string.IsNullOrEmpty(parts[1]) &&
+#if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER
+ !parts[1].StartsWith('/'))
+#else
+ !parts[1].StartsWith("/", StringComparison.Ordinal))
+#endif
+ {
+ fragment = $"#{parts[1]}";
+ return true;
+ }
+
+ return false;
+ }
+
///
/// Reads the stream input and parses it into an Open API document.
///
@@ -830,6 +857,34 @@ static bool AddToDictionary(IDictionary dict, string key
// Register only if it was actually added to the collection
return added && (Workspace?.RegisterComponentForDocument(this, componentToRegister, id) ?? false);
}
+
+ ///
+ /// Finds an operation in the document by its operation ID.
+ ///
+ /// The operation ID to search for.
+ /// The matching , or if not found.
+ public OpenApiOperation? GetOperationById(string operationId)
+ {
+ Utils.CheckArgumentNullOrEmpty(operationId);
+
+ return GetOperationByIdFromPathItems(Paths, operationId) ??
+ (Webhooks is not null ?
+ GetOperationByIdFromPathItems(Webhooks, operationId) : null);
+ }
+
+ private static OpenApiOperation? GetOperationByIdFromPathItems(IDictionary pathItems, string operationId)
+ {
+ foreach (var pathItem in pathItems.Values)
+ {
+ if (pathItem.Operations is null) continue;
+ foreach (var operation in pathItem.Operations.Values)
+ {
+ if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal))
+ return operation;
+ }
+ }
+ return null;
+ }
}
internal class FindSchemaReferences : OpenApiVisitorBase
diff --git a/src/Microsoft.OpenApi/Models/OpenApiLicense.cs b/src/Microsoft.OpenApi/Models/OpenApiLicense.cs
index 6645cc8d3..4c9a32945 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiLicense.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiLicense.cs
@@ -73,6 +73,7 @@ public virtual void SerializeAsV31(IOpenApiWriter writer)
public virtual void SerializeAsV3(IOpenApiWriter writer)
{
WriteInternal(writer, OpenApiSpecVersion.OpenApi3_0);
+ writer.WriteProperty(OpenApiConstants.OaiLicenseIdentifier, Identifier);
writer.WriteEndObject();
}
@@ -82,6 +83,7 @@ public virtual void SerializeAsV3(IOpenApiWriter writer)
public virtual void SerializeAsV2(IOpenApiWriter writer)
{
WriteInternal(writer, OpenApiSpecVersion.OpenApi2_0);
+ writer.WriteProperty(OpenApiConstants.OaiLicenseIdentifier, Identifier);
writer.WriteEndObject();
}
diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs
index e4625ff1d..ee1afcc0a 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs
@@ -75,7 +75,7 @@ public ISet? Tags
public IOpenApiRequestBody? RequestBody { get; set; }
///
- /// REQUIRED. The list of possible responses as they are returned from executing this operation.
+ /// The list of possible responses as they are returned from executing this operation.
///
public OpenApiResponses? Responses { get; set; } = [];
diff --git a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs
index d6b060a78..24c8db173 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs
@@ -8,6 +8,7 @@
namespace Microsoft.OpenApi
{
+#pragma warning disable CS0618
///
/// Parameter Object.
///
@@ -32,6 +33,7 @@ public class OpenApiParameter : IOpenApiExtensible, IOpenApiParameter
public bool Deprecated { get; set; }
///
+ [Obsolete("Use of AllowEmptyValue is not recommended and it is likely to be removed in a later revision.")]
public bool AllowEmptyValue { get; set; }
///
diff --git a/src/Microsoft.OpenApi/Models/OpenApiPaths.cs b/src/Microsoft.OpenApi/Models/OpenApiPaths.cs
index 18e67c313..1aa163e3c 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiPaths.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiPaths.cs
@@ -17,6 +17,9 @@ public OpenApiPaths() { }
/// Initializes a copy of object
///
/// The .
+ ///
+ /// This creates a shallow copy, the path items are the same reference as in the provided parameter.
+ ///
public OpenApiPaths(OpenApiPaths paths) : base(dictionary: paths) { }
}
}
diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs
index 4ee39336e..6bd02af25 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs
@@ -73,7 +73,7 @@ public virtual void SerializeAsV3(IOpenApiWriter writer)
SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer));
}
- private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
+ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
Action callback)
{
Utils.CheckArgumentNull(writer);
@@ -87,7 +87,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
}
// description
- writer.WriteRequiredProperty(OpenApiConstants.Description, Description);
+ writer.WriteProperty(OpenApiConstants.Description, Description);
// headers
writer.WriteOptionalMap(OpenApiConstants.Headers, Headers, callback);
@@ -120,7 +120,7 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
writer.WriteStartObject();
// description
- writer.WriteRequiredProperty(OpenApiConstants.Description, Description);
+ writer.WriteProperty(OpenApiConstants.Description, Description);
var extensionsClone = Extensions is not null ? new Dictionary(Extensions) : null;
@@ -177,7 +177,7 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
// so remove it from the cloned collection so we don't write it again.
extensionsClone?.Remove(key);
}
- }
+ }
}
}
diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
index 40f24bdd0..79bd88dd5 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
@@ -9,6 +9,7 @@
namespace Microsoft.OpenApi
{
+#pragma warning disable CS0618
///
/// The Schema Object allows the definition of input and output data types.
///
@@ -18,8 +19,10 @@ namespace Microsoft.OpenApi
/// - Serialization: To produce something functionally equivalent to boolean schemas, create an empty
/// for "true" behavior, or create a schema with only set to an empty schema for "false" behavior.
///
- public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer
+ public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaMissingProperties, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer
{
+ private static readonly IEnumerable s_singleNullElementList = [JsonNullSentinel.JsonNull];
+
///
public string? Title { get; set; }
@@ -44,6 +47,9 @@ public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaW
///
public IDictionary? Definitions { get; set; }
+ ///
+ public string? Anchor { get; set; }
+
private string? _exclusiveMaximum;
///
public string? ExclusiveMaximum
@@ -105,16 +111,23 @@ public string? ExclusiveMinimum
///
public JsonSchemaType? Type { get; set; }
- // x-nullable is filtered out by deserializers, but keep the check here in case it gets added from user code.
- private bool IsNullable =>
- (Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
- Extensions is not null &&
- Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) &&
- nullExtRawValue is JsonNodeExtension { Node: JsonNode jsonNode } &&
- jsonNode.GetValueKind() is JsonValueKind.True;
+ private bool HasNullType
+ => Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null);
+
+ internal bool WasConstExplicitlySet { get; private set; }
///
- public string? Const { get; set; }
+ public string? Const
+ {
+ get => field;
+ set
+ {
+ // TODO: In the next major release, Const should be made a JsonNode.
+ // See https://github.com/microsoft/OpenAPI.NET/issues/2935 for more information.
+ WasConstExplicitlySet = true;
+ field = value;
+ }
+ }
///
public string? Format { get; set; }
@@ -207,6 +220,15 @@ public string? Minimum
///
public bool? UniqueItems { get; set; }
+ ///
+ public IOpenApiSchema? Contains { get; set; }
+
+ ///
+ public uint? MaxContains { get; set; }
+
+ ///
+ public uint? MinContains { get; set; }
+
///
public IDictionary? Properties { get; set; }
@@ -229,6 +251,7 @@ public string? Minimum
public OpenApiDiscriminator? Discriminator { get; set; }
///
+ [Obsolete("Use Examples instead.")]
public JsonNode? Example { get; set; }
///
@@ -243,6 +266,30 @@ public string? Minimum
///
public IOpenApiSchema? UnevaluatedPropertiesSchema { get; set; }
+ ///
+ public string? ContentEncoding { get; set; }
+
+ ///
+ public string? ContentMediaType { get; set; }
+
+ ///
+ public IOpenApiSchema? ContentSchema { get; set; }
+
+ ///
+ public IOpenApiSchema? PropertyNames { get; set; }
+
+ ///
+ public IDictionary? DependentSchemas { get; set; }
+
+ ///
+ public IOpenApiSchema? If { get; set; }
+
+ ///
+ public IOpenApiSchema? Then { get; set; }
+
+ ///
+ public IOpenApiSchema? Else { get; set; }
+
///
public OpenApiExternalDocs? ExternalDocs { get; set; }
@@ -279,16 +326,43 @@ internal OpenApiSchema(IOpenApiSchema schema)
Title = schema.Title ?? Title;
Id = schema.Id ?? Id;
Const = schema.Const ?? Const;
+ if (schema is OpenApiSchema concreteSchema)
+ {
+ WasConstExplicitlySet = concreteSchema.WasConstExplicitlySet;
+ }
+ else if (Const is null)
+ {
+ WasConstExplicitlySet = false;
+ }
+
Schema = schema.Schema ?? Schema;
Comment = schema.Comment ?? Comment;
Vocabulary = schema.Vocabulary != null ? new Dictionary(schema.Vocabulary) : null;
+ if (schema is IOpenApiSchemaMissingProperties { Anchor: not null } missingPropertiesWithAnchor)
+ {
+ Anchor = missingPropertiesWithAnchor.Anchor;
+ }
DynamicAnchor = schema.DynamicAnchor ?? DynamicAnchor;
DynamicRef = schema.DynamicRef ?? DynamicRef;
Definitions = schema.Definitions != null ? new Dictionary(schema.Definitions) : null;
UnevaluatedProperties = schema.UnevaluatedProperties;
- if (schema is IOpenApiSchemaWithUnevaluatedProperties { UnevaluatedPropertiesSchema: { } unevaluatedSchema })
+ if (schema is IOpenApiSchemaMissingProperties missingProperties)
{
- UnevaluatedPropertiesSchema = unevaluatedSchema.CreateShallowCopy();
+ Contains = missingProperties.Contains?.CreateShallowCopy();
+ MaxContains = missingProperties.MaxContains ?? MaxContains;
+ MinContains = missingProperties.MinContains ?? MinContains;
+ if (missingProperties.UnevaluatedPropertiesSchema is { } unevaluatedSchema)
+ {
+ UnevaluatedPropertiesSchema = unevaluatedSchema.CreateShallowCopy();
+ }
+ ContentEncoding = missingProperties.ContentEncoding ?? ContentEncoding;
+ ContentMediaType = missingProperties.ContentMediaType ?? ContentMediaType;
+ ContentSchema = missingProperties.ContentSchema?.CreateShallowCopy();
+ PropertyNames = missingProperties.PropertyNames?.CreateShallowCopy();
+ DependentSchemas = missingProperties.DependentSchemas != null ? new Dictionary(missingProperties.DependentSchemas) : null;
+ If = missingProperties.If?.CreateShallowCopy();
+ Then = missingProperties.Then?.CreateShallowCopy();
+ Else = missingProperties.Else?.CreateShallowCopy();
}
ExclusiveMaximum = schema.ExclusiveMaximum ?? ExclusiveMaximum;
ExclusiveMinimum = schema.ExclusiveMinimum ?? ExclusiveMinimum;
@@ -404,7 +478,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
if (version >= OpenApiSpecVersion.OpenApi3_1)
{
- WriteJsonSchemaKeywords(writer);
+ WriteJsonSchemaKeywords(writer, callback);
}
// title
@@ -456,39 +530,36 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
// enum
var enumValue = Enum is not { Count: > 0 }
- && !string.IsNullOrEmpty(Const)
+ && WasConstExplicitlySet
&& version < OpenApiSpecVersion.OpenApi3_1
? new List { JsonValue.Create(Const)! }
: Enum;
writer.WriteOptionalCollection(OpenApiConstants.Enum, enumValue, (nodeWriter, s) => nodeWriter.WriteAny(s));
- // Handle oneOf/anyOf with null type for v3.0 downcast
- IList? effectiveOneOf = OneOf;
- IList? effectiveAnyOf = AnyOf;
- bool hasNullInComposition = false;
- JsonSchemaType? inferredType = null;
-
if (version == OpenApiSpecVersion.OpenApi3_0)
{
- (effectiveOneOf, var inferredOneOf, var nullInOneOf) = ProcessCompositionForNull(OneOf);
- hasNullInComposition |= nullInOneOf;
- inferredType = inferredOneOf ?? inferredType;
- (effectiveAnyOf, var inferredAnyOf, var nullInAnyOf) = ProcessCompositionForNull(AnyOf);
- hasNullInComposition |= nullInAnyOf;
- inferredType = inferredAnyOf ?? inferredType;
+ // If we have a schema that's only just { "type": "null" }, we serialize it as enum with null value.
+ if (Type == JsonSchemaType.Null &&
+ OneOf is not { Count: > 0 } &&
+ AnyOf is not { Count: > 0 } &&
+ AllOf is not { Count: > 0 } &&
+ Enum is not { Count: > 0 })
+ {
+ writer.WriteOptionalCollection(OpenApiConstants.Enum, s_singleNullElementList, (nodeWriter, s) => nodeWriter.WriteAny(s));
+ }
}
// type
- SerializeTypeProperty(writer, version, inferredType);
+ SerializeTypePropertyForVersion3AndLater(writer, version, callback);
// allOf
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback);
// anyOf
- writer.WriteOptionalCollection(OpenApiConstants.AnyOf, effectiveAnyOf, callback);
+ writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, callback);
// oneOf
- writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);
+ writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, callback);
// not
writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback);
@@ -526,7 +597,12 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
// nullable
if (version == OpenApiSpecVersion.OpenApi3_0)
{
- SerializeNullable(writer, version, hasNullInComposition);
+ // https://spec.openapis.org/oas/v3.0.4.html#fixed-fields-20
+ // This keyword only takes effect if type is explicitly defined within the same Schema Object.
+ //
+ // We don't care to avoid an unnecessary serialization.
+ // So, we attempt to serialize it regardless of whether or not a type property was serialized.
+ SerializeNullable(writer, version);
}
// discriminator
@@ -545,7 +621,10 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, callback);
// example
- writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e));
+ writer.WriteOptionalObject(
+ OpenApiConstants.Example,
+ version < OpenApiSpecVersion.OpenApi3_1 ? GetCompatibilityExample() : Example,
+ (w, e) => w.WriteAny(e));
// deprecated
writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false);
@@ -578,6 +657,8 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
{
writer.WriteOptionalMap(OpenApiConstants.PatternPropertiesExtension, PatternProperties, callback);
}
+
+ WriteV3CompatibilityKeywords(writer, callback);
}
// extensions
@@ -599,17 +680,23 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
SerializeAsV2(writer: writer, parentRequiredProperties: new HashSet(), propertyName: null);
}
- internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
+ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer, Action callback)
{
writer.WriteProperty(OpenApiConstants.Id, Id);
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema?.ToString());
writer.WriteProperty(OpenApiConstants.Comment, Comment);
- writer.WriteProperty(OpenApiConstants.Const, Const);
+
+ if (WasConstExplicitlySet)
+ {
+ writer.WriteRequiredProperty(OpenApiConstants.Const, Const);
+ }
+
writer.WriteOptionalMap(OpenApiConstants.Vocabulary, Vocabulary, (w, s) => w.WriteValue(s));
- writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV31(w));
+ writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, callback);
+ writer.WriteProperty(OpenApiConstants.Anchor, Anchor);
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);
-
+
// UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean.
// Only emit when the type could include objects.
// Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
@@ -620,7 +707,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
writer.WriteOptionalObject(
OpenApiConstants.UnevaluatedProperties,
UnevaluatedPropertiesSchema,
- (w, s) => s.SerializeAsV31(w));
+ callback);
}
else if (!UnevaluatedProperties)
{
@@ -628,8 +715,42 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
}
}
writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (nodeWriter, s) => nodeWriter.WriteAny(s));
- writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w));
+ writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, callback);
writer.WriteOptionalMap(OpenApiConstants.DependentRequired, DependentRequired, (w, s) => w.WriteValue(s));
+
+ // contains
+ writer.WriteOptionalObject(OpenApiConstants.Contains, Contains, callback);
+
+ // maxContains
+ writer.WriteProperty(OpenApiConstants.MaxContains, MaxContains);
+
+ // minContains
+ writer.WriteProperty(OpenApiConstants.MinContains, MinContains);
+ writer.WriteProperty(OpenApiConstants.ContentEncoding, ContentEncoding);
+ writer.WriteProperty(OpenApiConstants.ContentMediaType, ContentMediaType);
+ writer.WriteOptionalObject(OpenApiConstants.ContentSchema, ContentSchema, callback);
+ writer.WriteOptionalObject(OpenApiConstants.PropertyNames, PropertyNames, callback);
+ writer.WriteOptionalMap(OpenApiConstants.DependentSchemas, DependentSchemas, callback);
+ writer.WriteOptionalObject(OpenApiConstants.If, If, callback);
+ writer.WriteOptionalObject(OpenApiConstants.Then, Then, callback);
+ writer.WriteOptionalObject(OpenApiConstants.Else, Else, callback);
+ }
+
+ private void WriteV3CompatibilityKeywords(IOpenApiWriter writer, Action callback)
+ {
+ writer.WriteProperty(OpenApiConstants.AnchorExtension, Anchor);
+ writer.WriteProperty(OpenApiConstants.ContentEncodingExtension, ContentEncoding);
+ writer.WriteProperty(OpenApiConstants.ContentMediaTypeExtension, ContentMediaType);
+ writer.WriteOptionalObject(OpenApiConstants.ContentSchemaExtension, ContentSchema, callback);
+ writer.WriteOptionalObject(OpenApiConstants.ContainsExtension, Contains, callback);
+ writer.WriteProperty(OpenApiConstants.MaxContainsExtension, MaxContains);
+ writer.WriteProperty(OpenApiConstants.MinContainsExtension, MinContains);
+ writer.WriteOptionalObject(OpenApiConstants.PropertyNamesExtension, PropertyNames, callback);
+ writer.WriteOptionalMap(OpenApiConstants.DependentSchemasExtension, DependentSchemas, callback);
+ writer.WriteOptionalObject(OpenApiConstants.IfExtension, If, callback);
+ writer.WriteOptionalObject(OpenApiConstants.ThenExtension, Then, callback);
+ writer.WriteOptionalObject(OpenApiConstants.ElseExtension, Else, callback);
+ writer.WriteOptionalCollection(OpenApiConstants.JsonSchemaExamplesExtension, GetCompatibilityExamplesExtension(), (nodeWriter, s) => nodeWriter.WriteAny(s));
}
internal void WriteAsItemsProperties(IOpenApiWriter writer)
@@ -717,7 +838,7 @@ private void SerializeAsV2(
writer.WriteStartObject();
// type
- SerializeTypeProperty(writer, OpenApiSpecVersion.OpenApi2_0);
+ SerializeTypePropertyForVersion2(writer);
// description
writer.WriteProperty(OpenApiConstants.Description, Description);
@@ -776,7 +897,7 @@ private void SerializeAsV2(
});
// enum
- var enumValue = Enum is not { Count: > 0 } && !string.IsNullOrEmpty(Const)
+ var enumValue = Enum is not { Count: > 0 } && WasConstExplicitlySet
? new List { JsonValue.Create(Const)! }
: Enum;
writer.WriteOptionalCollection(OpenApiConstants.Enum, enumValue, (nodeWriter, s) => nodeWriter.WriteAny(s));
@@ -799,6 +920,7 @@ private void SerializeAsV2(
// oneOf (Not Supported in V2) - Write the first schema only as an allOf.
writer.WriteOptionalCollection(OpenApiConstants.AllOf, OneOf?.Take(1), (w, s) => s.SerializeAsV2(w));
}
+#pragma warning restore CS0618
}
// properties
@@ -842,7 +964,7 @@ private void SerializeAsV2(
writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, s) => s.SerializeAsV2(w));
// example
- writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e));
+ writer.WriteOptionalObject(OpenApiConstants.Example, GetCompatibilityExample(), (w, e) => w.WriteAny(e));
// x-nullable extension
SerializeNullable(writer, OpenApiSpecVersion.OpenApi2_0);
@@ -873,119 +995,162 @@ private void SerializeAsV2(
writer.WriteOptionalMap(OpenApiConstants.PatternPropertiesExtension, PatternProperties, (w, s) => s.SerializeAsV2(w));
}
+ writer.WriteOptionalCollection(OpenApiConstants.JsonSchemaExamplesExtension, GetCompatibilityExamplesExtension(), (nodeWriter, s) => nodeWriter.WriteAny(s));
+
// extensions
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0);
writer.WriteEndObject();
}
- private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version, JsonSchemaType? inferredType = null)
+ private void SerializeTypePropertyForVersion2(IOpenApiWriter writer)
{
- // Use original type or inferred type when the explicit type is not set
- var typeToUse = Type ?? inferredType;
-
- if (typeToUse is null)
+ if (Type is not { } type || type == JsonSchemaType.Null)
{
return;
}
- var unifiedType = IsNullable ? typeToUse.Value | JsonSchemaType.Null : typeToUse.Value;
- var typeWithoutNull = unifiedType & ~JsonSchemaType.Null;
-
- switch (version)
+ var typeWithoutNull = type & ~JsonSchemaType.Null;
+ if (!HasMultipleTypes(typeWithoutNull))
{
- case OpenApiSpecVersion.OpenApi2_0 or OpenApiSpecVersion.OpenApi3_0:
- if (typeWithoutNull != 0 && !HasMultipleTypes(typeWithoutNull))
- {
- writer.WriteProperty(OpenApiConstants.Type, typeWithoutNull.ToFirstIdentifier());
- }
- break;
- default:
- WriteUnifiedSchemaType(unifiedType, writer);
- break;
+ writer.WriteProperty(OpenApiConstants.Type, typeWithoutNull.ToFirstIdentifier());
}
}
- private static bool IsPowerOfTwo(int x)
- {
- return x != 0 && (x & (x - 1)) == 0;
- }
-
- private static bool HasMultipleTypes(JsonSchemaType schemaType)
+ ///
+ /// Serializes the "type" property for OpenAPI v3 and later versions,
+ /// falling back to anyOf/oneOf when multiple types cannot be expressed
+ /// using the "type" property alone (OpenAPI 3.0).
+ ///
+ private void SerializeTypePropertyForVersion3AndLater(IOpenApiWriter writer, OpenApiSpecVersion version, Action callback)
{
- var schemaTypeNumeric = (int)schemaType;
- return !IsPowerOfTwo(schemaTypeNumeric);
- }
+ if (Type is not { } type)
+ {
+ return;
+ }
- private static void WriteUnifiedSchemaType(JsonSchemaType type, IOpenApiWriter writer)
- {
- var array = (from JsonSchemaType flag in jsonSchemaTypeValues
- where type.HasFlag(flag)
- select flag.ToFirstIdentifier()).ToArray();
- if (array.Length > 1)
+ if (version == OpenApiSpecVersion.OpenApi3_0)
{
- writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) =>
+ if (type == JsonSchemaType.Null)
{
- if (!string.IsNullOrEmpty(s) && s is not null)
+ return;
+ }
+
+ var typeWithoutNull = type & ~JsonSchemaType.Null;
+ var hasNull = typeWithoutNull != type;
+ var arrayWithoutNull = (from JsonSchemaType flag in jsonSchemaTypeValues
+ where typeWithoutNull.HasFlag(flag)
+ select flag).ToArray();
+
+ // - If we have more than one type (excluding null), we have to use anyOf/oneOf.
+ // - If we have exactly one type alone (without null), we emit the type property.
+ // - If we have exactly one non-null type and also we have the null type, we emit the type property and nullable: true (handled in SerializeNullable)
+ if (arrayWithoutNull.Length > 1)
+ {
+ // If the schema doesn't already have anyOf/oneOf, we can write multiple types as such.
+ var canWriteAsAnyOf = AnyOf is not { Count: > 0 };
+ var canWriteAsOneOf = OneOf is not { Count: > 0 };
+ if (canWriteAsAnyOf)
+ {
+ writer.WriteOptionalCollection(OpenApiConstants.AnyOf, ConstructChildSchemasForTypes(arrayWithoutNull, hasNull), callback);
+ return;
+ }
+ else if (canWriteAsOneOf)
{
- w.WriteValue(s);
+ writer.WriteOptionalCollection(OpenApiConstants.OneOf, ConstructChildSchemasForTypes(arrayWithoutNull, hasNull), callback);
+ return;
}
- });
+ }
+ else if (arrayWithoutNull.Length == 1)
+ {
+ writer.WriteProperty(OpenApiConstants.Type, arrayWithoutNull[0].ToSingleIdentifier());
+ return;
+ }
}
else
{
- writer.WriteProperty(OpenApiConstants.Type, array[0]);
+ var array = (from JsonSchemaType flag in jsonSchemaTypeValues
+ where type.HasFlag(flag)
+ select flag).ToArray();
+
+ if (array.Length > 1)
+ {
+ writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) => w.WriteValue(s.ToSingleIdentifier()));
+ }
+ else if (array.Length == 1)
+ {
+ writer.WriteProperty(OpenApiConstants.Type, array[0].ToSingleIdentifier());
+ }
}
- }
- private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version, bool hasNullInComposition = false)
- {
- if (IsNullable || hasNullInComposition)
+ static OpenApiSchema[] ConstructChildSchemasForTypes(JsonSchemaType[] types, bool hasNull)
{
- switch (version)
+ var schemas = new OpenApiSchema[types.Length + (hasNull ? 1 : 0)];
+ for (int i = 0; i < types.Length; i++)
{
- case OpenApiSpecVersion.OpenApi2_0:
- writer.WriteProperty(OpenApiConstants.NullableExtension, true);
- break;
- case OpenApiSpecVersion.OpenApi3_0:
- writer.WriteProperty(OpenApiConstants.Nullable, true);
- break;
+ schemas[i] = new OpenApiSchema()
+ {
+ Type = types[i]
+ };
}
+
+ if (hasNull)
+ {
+ schemas[schemas.Length - 1] = new OpenApiSchema() { Type = JsonSchemaType.Null };
+ }
+
+ return schemas;
}
}
- ///
- /// Processes a composition (oneOf or anyOf) for null types, filtering out null schemas and inferring common type.
- ///
- /// The list of schemas in the composition.
- /// A tuple with the effective list, inferred type, and whether null is present in composition.
- private static (IList? effective, JsonSchemaType? inferredType, bool hasNullInComposition)
- ProcessCompositionForNull(IList? composition)
+ private JsonNode? GetCompatibilityExample()
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ return Example ?? Examples?.FirstOrDefault();
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ private IEnumerable? GetCompatibilityExamplesExtension()
{
- if (composition is null || !composition.Any(static s => s.Type is JsonSchemaType.Null))
+ if (Examples is null || Examples.Count == 0)
{
- // Nothing to patch
- return (composition, null, false);
+ return null;
}
- var nonNullSchemas = composition
- .Where(static s => s.Type is null or not JsonSchemaType.Null)
- .ToList();
-
- if (nonNullSchemas.Count > 0)
+#pragma warning disable CS0618 // Type or member is obsolete
+ if (Example is not null)
{
- JsonSchemaType commonType = 0;
+ return Examples;
+ }
+#pragma warning restore CS0618 // Type or member is obsolete
- foreach (var schema in nonNullSchemas)
- {
- commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null;
- }
+ return Examples.Count > 1 ? Examples.Skip(1) : null;
+ }
- return (nonNullSchemas, commonType, true);
- }
- else
+ private static bool IsPowerOfTwo(int x)
+ {
+ return x != 0 && (x & (x - 1)) == 0;
+ }
+
+ private static bool HasMultipleTypes(JsonSchemaType schemaType)
+ {
+ var schemaTypeNumeric = (int)schemaType;
+ return !IsPowerOfTwo(schemaTypeNumeric);
+ }
+
+ private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version)
+ {
+ if (HasNullType)
{
- return (null, null, true);
+ switch (version)
+ {
+ case OpenApiSpecVersion.OpenApi2_0:
+ writer.WriteProperty(OpenApiConstants.NullableExtension, true);
+ break;
+ case OpenApiSpecVersion.OpenApi3_0:
+ writer.WriteProperty(OpenApiConstants.Nullable, true);
+ break;
+ }
}
}
diff --git a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
index 626f7f2bd..225910968 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs
@@ -97,6 +97,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
Action callback)
{
Utils.CheckArgumentNull(writer);
+ EnsureRequiredPropertiesAreSet(version);
writer.WriteStartObject();
@@ -132,7 +133,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
}
else
{
- writer.WriteProperty("x-oauth2-metadata-url", OAuth2MetadataUrl?.ToString());
+ writer.WriteProperty(OpenApiConstants.OAuth2MetadataUrlExtension, OAuth2MetadataUrl?.ToString());
}
writer.WriteOptionalObject(OpenApiConstants.Flows, Flows, callback);
break;
@@ -176,6 +177,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
public virtual void SerializeAsV2(IOpenApiWriter writer)
{
Utils.CheckArgumentNull(writer);
+ EnsureRequiredPropertiesAreSet(OpenApiSpecVersion.OpenApi2_0);
if (Type == SecuritySchemeType.Http && Scheme != OpenApiConstants.Basic)
{
@@ -239,6 +241,25 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
writer.WriteEndObject();
}
+ private void EnsureRequiredPropertiesAreSet(OpenApiSpecVersion version)
+ {
+ switch (Type)
+ {
+ case null:
+ throw new OpenApiException("The 'type' property is required for security schemes.");
+ case SecuritySchemeType.ApiKey when string.IsNullOrEmpty(Name):
+ throw new OpenApiException("The 'name' property is required for apiKey security schemes.");
+ case SecuritySchemeType.ApiKey when In is null:
+ throw new OpenApiException("The 'in' property is required for apiKey security schemes.");
+ case SecuritySchemeType.Http when version >= OpenApiSpecVersion.OpenApi3_0 && string.IsNullOrEmpty(Scheme):
+ throw new OpenApiException("The 'scheme' property is required for http security schemes.");
+ case SecuritySchemeType.OAuth2 when Flows is null:
+ throw new OpenApiException("The 'flows' property is required for oauth2 security schemes.");
+ case SecuritySchemeType.OpenIdConnect when version >= OpenApiSpecVersion.OpenApi3_0 && OpenIdConnectUrl is null:
+ throw new OpenApiException("The 'openIdConnectUrl' property is required for openIdConnect security schemes.");
+ }
+ }
+
///
/// Arbitrarily chooses one object from the
/// to populate in V2 security scheme.
diff --git a/src/Microsoft.OpenApi/Models/OpenApiXml.cs b/src/Microsoft.OpenApi/Models/OpenApiXml.cs
index 82d7c2180..6e4e88962 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiXml.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiXml.cs
@@ -30,7 +30,7 @@ public class OpenApiXml : IOpenApiSerializable, IOpenApiExtensible
/// Declares whether the property definition translates to an attribute instead of an element.
/// Default value is false.
///
- [Obsolete("Use NodeType property instead. This property will be removed in a future version.")]
+ [Obsolete("Use NodeType set to OpenApiXmlNodeType.Attribute instead. This property will be removed in a future version.")]
internal bool Attribute
{
get
@@ -47,7 +47,7 @@ internal bool Attribute
/// Signifies whether the array is wrapped.
/// Default value is false.
///
- [Obsolete("Use NodeType property instead. This property will be removed in a future version.")]
+ [Obsolete("Use NodeType set to OpenApiXmlNodeType.Element instead. This property will be removed in a future version.")]
internal bool Wrapped
{
get
diff --git a/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs b/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs
index ed2936bd4..2af750476 100644
--- a/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs
+++ b/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
namespace Microsoft.OpenApi;
///
@@ -10,6 +11,11 @@ namespace Microsoft.OpenApi;
/// The type for the reference holding the additional fields and annotations
public abstract class BaseOpenApiReferenceHolder : IOpenApiReferenceHolder where T : class, IOpenApiReferenceable, U where U : IOpenApiReferenceable, IOpenApiSerializable where V : BaseOpenApiReference, new()
{
+ [ThreadStatic]
+ private static HashSet>? t_activeReferenceAccesses;
+ [ThreadStatic]
+ private static HashSet>? t_activeTargetActions;
+
///
public virtual U? Target
{
@@ -19,28 +25,129 @@ public virtual U? Target
return Reference.HostDocument.ResolveReferenceTo(Reference, this as IOpenApiSchema);
}
}
+
+ ///
+ /// Gets a value from the resolved target while detecting cycles in delegated member access.
+ ///
+ /// The type of value to get from the target.
+ /// Selects the value from the resolved target.
+ /// The selected value, or the default value when the target cannot be resolved.
+ ///
+ /// The guard remains active while reads the target member. This covers
+ /// the complete delegated call chain without changing the immediate-resolution semantics of
+ /// or walking an acyclic chain more than once.
+ ///
+ private protected TResult GetFromTarget(Func selector)
+ {
+ Utils.CheckArgumentNull(selector);
+ return ExecuteWithReferenceAccessGuard(this, () =>
+ {
+ return Target is { } target ? selector(target) : default!;
+ });
+ }
+
+ ///
+ /// Executes an action against the resolved target while detecting cycles in delegated access.
+ ///
+ /// The action to execute against the resolved target.
+ private protected void ApplyToTarget(Action action)
+ {
+ Utils.CheckArgumentNull(action);
+ ExecuteWithTargetActionGuard