diff --git a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs
index f1300f027..4f9bb1e34 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs
@@ -44,7 +44,7 @@ public ParameterStyle? Style
///
public bool Explode
{
- get => _explode ?? Style == ParameterStyle.Form;
+ get => _explode ?? (Style is ParameterStyle.Form or ParameterStyle.Cookie);
set => _explode = value;
}
@@ -115,6 +115,12 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio
Action callback)
{
Utils.CheckArgumentNull(writer);
+
+ // Validate that Cookie style is only used in OpenAPI 3.2 and later
+ if (Style == ParameterStyle.Cookie && version < OpenApiSpecVersion.OpenApi3_2)
+ {
+ throw new OpenApiException($"Parameter style 'cookie' is only supported in OpenAPI 3.2 and later versions. Current version: {version}");
+ }
writer.WriteStartObject();
@@ -143,7 +149,7 @@ internal void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion versio
}
// explode
- writer.WriteProperty(OpenApiConstants.Explode, _explode, Style is ParameterStyle.Form);
+ writer.WriteProperty(OpenApiConstants.Explode, _explode, Style is ParameterStyle.Form or ParameterStyle.Cookie);
// allowReserved
writer.WriteProperty(OpenApiConstants.AllowReserved, AllowReserved, false);
@@ -251,6 +257,12 @@ internal virtual void WriteRequestBodySchemaForV2(IOpenApiWriter writer, Diction
public virtual void SerializeAsV2(IOpenApiWriter writer)
{
Utils.CheckArgumentNull(writer);
+
+ // Validate that Cookie style is only used in OpenAPI 3.2 and later
+ if (Style == ParameterStyle.Cookie)
+ {
+ throw new OpenApiException($"Parameter style 'cookie' is only supported in OpenAPI 3.2 and later versions. Current version: {OpenApiSpecVersion.OpenApi2_0}");
+ }
writer.WriteStartObject();
diff --git a/src/Microsoft.OpenApi/Models/ParameterStyle.cs b/src/Microsoft.OpenApi/Models/ParameterStyle.cs
index 3edb049a5..f948fc6fb 100644
--- a/src/Microsoft.OpenApi/Models/ParameterStyle.cs
+++ b/src/Microsoft.OpenApi/Models/ParameterStyle.cs
@@ -41,6 +41,11 @@ public enum ParameterStyle
///
/// Provides a simple way of rendering nested objects using form parameters.
///
- [Display("deepObject")] DeepObject
+ [Display("deepObject")] DeepObject,
+
+ ///
+ /// Cookie style parameters. Introduced in OpenAPI 3.2.
+ ///
+ [Display("cookie")] Cookie
}
}
diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterCookieStyleTests.SerializeCookieParameterAsV32JsonWorksAsync_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterCookieStyleTests.SerializeCookieParameterAsV32JsonWorksAsync_produceTerseOutput=False.verified.txt
new file mode 100644
index 000000000..7e87bc43e
--- /dev/null
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterCookieStyleTests.SerializeCookieParameterAsV32JsonWorksAsync_produceTerseOutput=False.verified.txt
@@ -0,0 +1,9 @@
+{
+ "name": "sessionId",
+ "in": "cookie",
+ "description": "Session identifier stored in cookie",
+ "style": "cookie",
+ "schema": {
+ "type": "string"
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterCookieStyleTests.SerializeCookieParameterAsV32JsonWorksAsync_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterCookieStyleTests.SerializeCookieParameterAsV32JsonWorksAsync_produceTerseOutput=True.verified.txt
new file mode 100644
index 000000000..f2b086e6b
--- /dev/null
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterCookieStyleTests.SerializeCookieParameterAsV32JsonWorksAsync_produceTerseOutput=True.verified.txt
@@ -0,0 +1 @@
+{"name":"sessionId","in":"cookie","description":"Session identifier stored in cookie","style":"cookie","schema":{"type":"string"}}
\ No newline at end of file
diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterCookieStyleTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterCookieStyleTests.cs
new file mode 100644
index 000000000..cf0348fa9
--- /dev/null
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterCookieStyleTests.cs
@@ -0,0 +1,248 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text.Json.Nodes;
+using System.Threading.Tasks;
+using Xunit;
+using VerifyXunit;
+
+namespace Microsoft.OpenApi.Tests.Models
+{
+ [Collection("DefaultSettings")]
+ public class OpenApiParameterCookieStyleTests
+ {
+ private static OpenApiParameter CookieParameter => new()
+ {
+ Name = "sessionId",
+ In = ParameterLocation.Cookie,
+ Style = ParameterStyle.Cookie,
+ Description = "Session identifier stored in cookie",
+ Schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String
+ }
+ };
+
+ private static OpenApiParameter CookieParameterWithDefault => new()
+ {
+ Name = "preferences",
+ In = ParameterLocation.Cookie,
+ Description = "User preferences stored in cookie",
+ Schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String
+ }
+ };
+
+ [Fact]
+ public void CookieParameterStyleIsAvailable()
+ {
+ // Arrange & Act
+ var parameter = CookieParameter;
+
+ // Assert
+ Assert.Equal(ParameterStyle.Cookie, parameter.Style);
+ Assert.Equal(ParameterLocation.Cookie, parameter.In);
+ }
+
+ [Fact]
+ public void CookieParameterHasCorrectDefaultStyle()
+ {
+ // Arrange & Act
+ var parameter = CookieParameterWithDefault;
+
+ // Assert
+ Assert.Equal(ParameterStyle.Form, parameter.Style); // Default for cookie location should be Form
+ }
+
+ [Fact]
+ public void CookieParameterStyleDisplayNameIsCookie()
+ {
+ // Arrange & Act
+ var displayName = ParameterStyle.Cookie.GetDisplayName();
+
+ // Assert
+ Assert.Equal("cookie", displayName);
+ }
+
+ [Fact]
+ public async Task SerializeCookieParameterAsV32JsonWorks()
+ {
+ // Arrange
+ var expected =
+ """
+ {
+ "name": "sessionId",
+ "in": "cookie",
+ "description": "Session identifier stored in cookie",
+ "style": "cookie",
+ "schema": {
+ "type": "string"
+ }
+ }
+ """;
+
+ // Act
+ var actual = await CookieParameter.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_2);
+
+ // Assert
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(actual), JsonNode.Parse(expected)));
+ }
+
+ [Fact]
+ public async Task SerializeCookieParameterWithDefaultStyleAsV32JsonWorks()
+ {
+ // Arrange
+ var expected =
+ """
+ {
+ "name": "preferences",
+ "in": "cookie",
+ "description": "User preferences stored in cookie",
+ "schema": {
+ "type": "string"
+ }
+ }
+ """;
+
+ // Act
+ var actual = await CookieParameterWithDefault.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_2);
+
+ // Assert
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(actual), JsonNode.Parse(expected)));
+ }
+
+ [Theory]
+ [InlineData(OpenApiSpecVersion.OpenApi2_0)]
+ [InlineData(OpenApiSpecVersion.OpenApi3_0)]
+ [InlineData(OpenApiSpecVersion.OpenApi3_1)]
+ public void SerializeCookieParameterStyleThrowsForEarlierVersions(OpenApiSpecVersion version)
+ {
+ // Arrange
+ var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
+ var writer = new OpenApiJsonWriter(outputStringWriter);
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ {
+ switch (version)
+ {
+ case OpenApiSpecVersion.OpenApi2_0:
+ CookieParameter.SerializeAsV2(writer);
+ break;
+ case OpenApiSpecVersion.OpenApi3_0:
+ CookieParameter.SerializeAsV3(writer);
+ break;
+ case OpenApiSpecVersion.OpenApi3_1:
+ CookieParameter.SerializeAsV31(writer);
+ break;
+ }
+ });
+
+ Assert.Contains("Parameter style 'cookie' is only supported in OpenAPI 3.2 and later versions", exception.Message);
+ Assert.Contains($"Current version: {version}", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(OpenApiSpecVersion.OpenApi2_0)]
+ [InlineData(OpenApiSpecVersion.OpenApi3_0)]
+ [InlineData(OpenApiSpecVersion.OpenApi3_1)]
+ public async Task SerializeCookieParameterWithDefaultStyleWorksForEarlierVersions(OpenApiSpecVersion version)
+ {
+ // Arrange & Act
+ string actual = version switch
+ {
+ OpenApiSpecVersion.OpenApi2_0 => await CookieParameterWithDefault.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi2_0),
+ OpenApiSpecVersion.OpenApi3_0 => await CookieParameterWithDefault.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0),
+ OpenApiSpecVersion.OpenApi3_1 => await CookieParameterWithDefault.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ // Assert - Should not throw because default style (Form) is being used
+ Assert.NotEmpty(actual);
+ Assert.DoesNotContain("\"style\"", actual); // Style should not be emitted when it's the default
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task SerializeCookieParameterAsV32JsonWorksAsync(bool produceTerseOutput)
+ {
+ // Arrange
+ var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
+ var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = produceTerseOutput });
+
+ // Act
+ CookieParameter.SerializeAsV32(writer);
+ await writer.FlushAsync();
+
+ // Assert
+ await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput);
+ }
+
+ [Fact]
+ public void CookieParameterStyleEnumValueExists()
+ {
+ // Arrange & Act
+ var cookieStyleExists = Enum.IsDefined(typeof(ParameterStyle), ParameterStyle.Cookie);
+
+ // Assert
+ Assert.True(cookieStyleExists);
+ }
+
+ [Fact]
+ public void CookieParameterStyleCanBeDeserialized()
+ {
+ // Arrange
+ var cookieStyleString = "cookie";
+
+ // Act
+ var success = cookieStyleString.TryGetEnumFromDisplayName(out var parameterStyle);
+
+ // Assert
+ Assert.True(success);
+ Assert.Equal(ParameterStyle.Cookie, parameterStyle);
+ }
+
+ [Fact]
+ public async Task SerializeCookieParameterAsYamlV32Works()
+ {
+ // Arrange
+ var expected = """
+ name: sessionId
+ in: cookie
+ description: Session identifier stored in cookie
+ style: cookie
+ schema:
+ type: string
+ """;
+
+ // Act
+ var actual = await CookieParameter.SerializeAsYamlAsync(OpenApiSpecVersion.OpenApi3_2);
+
+ // Assert
+ Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), actual.MakeLineBreaksEnvironmentNeutral());
+ }
+
+ [Theory]
+ [InlineData(ParameterStyle.Form, true)]
+ [InlineData(ParameterStyle.SpaceDelimited, false)]
+ [InlineData(ParameterStyle.Cookie, true)]
+ public void WhenStyleIsFormOrCookieTheDefaultValueOfExplodeShouldBeTrueOtherwiseFalse(ParameterStyle? style, bool expectedExplode)
+ {
+ // Arrange
+ var parameter = new OpenApiParameter
+ {
+ Name = "name1",
+ In = ParameterLocation.Query,
+ Style = style
+ };
+
+ // Act & Assert
+ Assert.Equal(expectedExplode, parameter.Explode);
+ }
+ }
+}
diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
index d2a93e2c8..1f98ad513 100644
--- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
+++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
@@ -1753,6 +1753,8 @@ namespace Microsoft.OpenApi
PipeDelimited = 5,
[Microsoft.OpenApi.Display("deepObject")]
DeepObject = 6,
+ [Microsoft.OpenApi.Display("cookie")]
+ Cookie = 7,
}
public sealed class PathExpression : Microsoft.OpenApi.SourceExpression
{
diff --git a/test/Microsoft.OpenApi.Tests/Reader/OpenApiParameterCookieStyleDeserializationTests.cs b/test/Microsoft.OpenApi.Tests/Reader/OpenApiParameterCookieStyleDeserializationTests.cs
new file mode 100644
index 000000000..6a98dea42
--- /dev/null
+++ b/test/Microsoft.OpenApi.Tests/Reader/OpenApiParameterCookieStyleDeserializationTests.cs
@@ -0,0 +1,196 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.OpenApi.Tests.Reader
+{
+ [Collection("DefaultSettings")]
+ public class OpenApiParameterCookieStyleDeserializationTests
+ {
+ [Fact]
+ public void DeserializeCookieParameterStyleFromJsonWorks()
+ {
+ // Arrange
+ var json = """
+ {
+ "openapi": "3.2.0",
+ "info": {
+ "title": "Test API",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/test": {
+ "get": {
+ "parameters": [
+ {
+ "name": "sessionId",
+ "in": "cookie",
+ "style": "cookie",
+ "description": "Session identifier stored in cookie",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ // Act
+ var result = OpenApiDocument.Parse(json, "json", SettingsFixture.ReaderSettings);
+
+ // Assert
+ Assert.NotNull(result.Document);
+ Assert.Empty(result.Diagnostic.Errors);
+
+ var parameter = result.Document.Paths["/test"].Operations[System.Net.Http.HttpMethod.Get].Parameters[0];
+ Assert.Equal("sessionId", parameter.Name);
+ Assert.Equal(ParameterLocation.Cookie, parameter.In);
+ Assert.Equal(ParameterStyle.Cookie, parameter.Style);
+ }
+
+ [Fact]
+ public void DeserializeCookieParameterWithDefaultStyleFromJsonWorks()
+ {
+ // Arrange
+ var json = """
+ {
+ "openapi": "3.2.0",
+ "info": {
+ "title": "Test API",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/test": {
+ "get": {
+ "parameters": [
+ {
+ "name": "preferences",
+ "in": "cookie",
+ "description": "User preferences stored in cookie",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ // Act
+ var result = OpenApiDocument.Parse(json, "json", SettingsFixture.ReaderSettings);
+
+ // Assert
+ Assert.NotNull(result.Document);
+ Assert.Empty(result.Diagnostic.Errors);
+
+ var parameter = result.Document.Paths["/test"].Operations[System.Net.Http.HttpMethod.Get].Parameters[0];
+ Assert.Equal("preferences", parameter.Name);
+ Assert.Equal(ParameterLocation.Cookie, parameter.In);
+ Assert.Equal(ParameterStyle.Form, parameter.Style); // Should default to Form for cookie location
+ }
+
+ [Fact]
+ public void DeserializeCookieParameterStyleFromYamlWorks()
+ {
+ // Arrange
+ var yaml = """
+ openapi: 3.2.0
+ info:
+ title: Test API
+ version: 1.0.0
+ paths:
+ /test:
+ get:
+ parameters:
+ - name: sessionId
+ in: cookie
+ style: cookie
+ description: Session identifier stored in cookie
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Success
+ """;
+
+ // Act
+ var result = OpenApiDocument.Parse(yaml, "yaml", SettingsFixture.ReaderSettings);
+
+ // Assert
+ Assert.NotNull(result.Document);
+ Assert.Empty(result.Diagnostic.Errors);
+
+ var parameter = result.Document.Paths["/test"].Operations[System.Net.Http.HttpMethod.Get].Parameters[0];
+ Assert.Equal("sessionId", parameter.Name);
+ Assert.Equal(ParameterLocation.Cookie, parameter.In);
+ Assert.Equal(ParameterStyle.Cookie, parameter.Style);
+ }
+
+ [Fact]
+ public async Task SerializeAndDeserializeCookieParameterRoundTrip()
+ {
+ // Arrange
+ var original = new OpenApiParameter
+ {
+ Name = "trackingId",
+ In = ParameterLocation.Cookie,
+ Style = ParameterStyle.Cookie,
+ Description = "Tracking identifier",
+ Schema = new OpenApiSchema { Type = JsonSchemaType.String }
+ };
+
+ var document = new OpenApiDocument
+ {
+ Info = new OpenApiInfo { Title = "Test", Version = "1.0.0" },
+ Paths = new OpenApiPaths
+ {
+ ["/test"] = new OpenApiPathItem
+ {
+ Operations = new()
+ {
+ [System.Net.Http.HttpMethod.Get] = new OpenApiOperation
+ {
+ Parameters = new[] { original },
+ Responses = new OpenApiResponses
+ {
+ ["200"] = new OpenApiResponse { Description = "Success" }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ // Act
+ var json = await document.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_2);
+ var result = OpenApiDocument.Parse(json, "json", SettingsFixture.ReaderSettings);
+
+ // Assert
+ Assert.NotNull(result.Document);
+ Assert.Empty(result.Diagnostic.Errors);
+
+ var parameter = result.Document.Paths["/test"].Operations[System.Net.Http.HttpMethod.Get].Parameters[0];
+ Assert.Equal(original.Name, parameter.Name);
+ Assert.Equal(original.In, parameter.In);
+ Assert.Equal(original.Style, parameter.Style);
+ Assert.Equal(original.Description, parameter.Description);
+ }
+ }
+}