Skip to content

Commit e4c14a4

Browse files
committed
fix: adds generic shallow copy method to avoid inadvertent conversions of references to schemas
Signed-off-by: Vincent Biret <[email protected]>
1 parent e3c80a3 commit e4c14a4

File tree

11 files changed

+107
-79
lines changed

11 files changed

+107
-79
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Microsoft.OpenApi.Interfaces;
2+
/// <summary>
3+
/// Interface for shallow copyable objects.
4+
/// </summary>
5+
/// <typeparam name="T">The type of the resulting object</typeparam>
6+
public interface IShallowCopyable<out T>
7+
{
8+
/// <summary>
9+
/// Create a shallow copy of the current instance.
10+
/// </summary>
11+
T CreateShallowCopy();
12+
}

src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.OpenApi.Models.Interfaces;
88
/// Defines the base properties for the schema object.
99
/// This interface is provided for type assertions but should not be implemented by package consumers beyond automatic mocking.
1010
/// </summary>
11-
public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiSerializable, IOpenApiReadOnlyExtensible
11+
public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiSerializable, IOpenApiReadOnlyExtensible, IShallowCopyable<IOpenApiSchema>
1212
{
1313

1414
/// <summary>

src/Microsoft.OpenApi/Models/OpenApiHeader.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public OpenApiHeader(IOpenApiHeader header)
7272
Style = header?.Style ?? Style;
7373
Explode = header?.Explode ?? Explode;
7474
AllowReserved = header?.AllowReserved ?? AllowReserved;
75-
Schema = header?.Schema != null ? new OpenApiSchema(header.Schema) : null;
75+
Schema = header?.Schema?.CreateShallowCopy();
7676
Example = header?.Example != null ? JsonNodeCloneHelper.Clone(header.Example) : null;
7777
Examples = header?.Examples != null ? new Dictionary<string, IOpenApiExample>(header.Examples) : null;
7878
Content = header?.Content != null ? new Dictionary<string, OpenApiMediaType>(header.Content) : null;

src/Microsoft.OpenApi/Models/OpenApiMediaType.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public OpenApiMediaType() { }
5959
/// </summary>
6060
public OpenApiMediaType(OpenApiMediaType? mediaType)
6161
{
62-
Schema = mediaType?.Schema != null ? new OpenApiSchema(mediaType.Schema) : null;
62+
Schema = mediaType?.Schema?.CreateShallowCopy();
6363
Example = mediaType?.Example != null ? JsonNodeCloneHelper.Clone(mediaType.Example) : null;
6464
Examples = mediaType?.Examples != null ? new Dictionary<string, IOpenApiExample>(mediaType.Examples) : null;
6565
Encoding = mediaType?.Encoding != null ? new Dictionary<string, OpenApiEncoding>(mediaType.Encoding) : null;

src/Microsoft.OpenApi/Models/OpenApiParameter.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public OpenApiParameter(IOpenApiParameter parameter)
9090
Style = parameter.Style ?? Style;
9191
Explode = parameter.Explode;
9292
AllowReserved = parameter.AllowReserved;
93-
Schema = parameter.Schema != null ? new OpenApiSchema(parameter.Schema) : null;
93+
Schema = parameter.Schema.CreateShallowCopy();
9494
Examples = parameter.Examples != null ? new Dictionary<string, IOpenApiExample>(parameter.Examples) : null;
9595
Example = parameter.Example != null ? JsonNodeCloneHelper.Clone(parameter.Example) : null;
9696
Content = parameter.Content != null ? new Dictionary<string, OpenApiMediaType>(parameter.Content) : null;

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

+60-53
Original file line numberDiff line numberDiff line change
@@ -186,60 +186,61 @@ public OpenApiSchema() { }
186186
/// Initializes a copy of <see cref="IOpenApiSchema"/> object
187187
/// </summary>
188188
/// <param name="schema">The schema object to copy from.</param>
189-
public OpenApiSchema(IOpenApiSchema schema)
189+
internal OpenApiSchema(IOpenApiSchema schema)
190190
{
191-
Title = schema?.Title ?? Title;
192-
Id = schema?.Id ?? Id;
193-
Const = schema?.Const ?? Const;
194-
Schema = schema?.Schema ?? Schema;
195-
Comment = schema?.Comment ?? Comment;
196-
Vocabulary = schema?.Vocabulary != null ? new Dictionary<string, bool>(schema.Vocabulary) : null;
197-
DynamicAnchor = schema?.DynamicAnchor ?? DynamicAnchor;
198-
DynamicRef = schema?.DynamicRef ?? DynamicRef;
199-
Definitions = schema?.Definitions != null ? new Dictionary<string, IOpenApiSchema>(schema.Definitions) : null;
200-
UnevaluatedProperties = schema?.UnevaluatedProperties ?? UnevaluatedProperties;
201-
V31ExclusiveMaximum = schema?.V31ExclusiveMaximum ?? V31ExclusiveMaximum;
202-
V31ExclusiveMinimum = schema?.V31ExclusiveMinimum ?? V31ExclusiveMinimum;
203-
Type = schema?.Type ?? Type;
204-
Format = schema?.Format ?? Format;
205-
Description = schema?.Description ?? Description;
206-
Maximum = schema?.Maximum ?? Maximum;
207-
ExclusiveMaximum = schema?.ExclusiveMaximum ?? ExclusiveMaximum;
208-
Minimum = schema?.Minimum ?? Minimum;
209-
ExclusiveMinimum = schema?.ExclusiveMinimum ?? ExclusiveMinimum;
210-
MaxLength = schema?.MaxLength ?? MaxLength;
211-
MinLength = schema?.MinLength ?? MinLength;
212-
Pattern = schema?.Pattern ?? Pattern;
213-
MultipleOf = schema?.MultipleOf ?? MultipleOf;
214-
Default = schema?.Default != null ? JsonNodeCloneHelper.Clone(schema?.Default) : null;
215-
ReadOnly = schema?.ReadOnly ?? ReadOnly;
216-
WriteOnly = schema?.WriteOnly ?? WriteOnly;
217-
AllOf = schema?.AllOf != null ? new List<IOpenApiSchema>(schema.AllOf) : null;
218-
OneOf = schema?.OneOf != null ? new List<IOpenApiSchema>(schema.OneOf) : null;
219-
AnyOf = schema?.AnyOf != null ? new List<IOpenApiSchema>(schema.AnyOf) : null;
220-
Not = schema?.Not != null ? new OpenApiSchema(schema?.Not) : null;
221-
Required = schema?.Required != null ? new HashSet<string>(schema.Required) : null;
222-
Items = schema?.Items != null ? new OpenApiSchema(schema?.Items) : null;
223-
MaxItems = schema?.MaxItems ?? MaxItems;
224-
MinItems = schema?.MinItems ?? MinItems;
225-
UniqueItems = schema?.UniqueItems ?? UniqueItems;
226-
Properties = schema?.Properties != null ? new Dictionary<string, IOpenApiSchema>(schema.Properties) : null;
227-
PatternProperties = schema?.PatternProperties != null ? new Dictionary<string, IOpenApiSchema>(schema.PatternProperties) : null;
228-
MaxProperties = schema?.MaxProperties ?? MaxProperties;
229-
MinProperties = schema?.MinProperties ?? MinProperties;
230-
AdditionalPropertiesAllowed = schema?.AdditionalPropertiesAllowed ?? AdditionalPropertiesAllowed;
231-
AdditionalProperties = schema?.AdditionalProperties != null ? new OpenApiSchema(schema?.AdditionalProperties) : null;
232-
Discriminator = schema?.Discriminator != null ? new(schema?.Discriminator) : null;
233-
Example = schema?.Example != null ? JsonNodeCloneHelper.Clone(schema?.Example) : null;
234-
Examples = schema?.Examples != null ? new List<JsonNode>(schema.Examples) : null;
235-
Enum = schema?.Enum != null ? new List<JsonNode>(schema.Enum) : null;
236-
Nullable = schema?.Nullable ?? Nullable;
237-
ExternalDocs = schema?.ExternalDocs != null ? new(schema?.ExternalDocs) : null;
238-
Deprecated = schema?.Deprecated ?? Deprecated;
239-
Xml = schema?.Xml != null ? new(schema?.Xml) : null;
240-
Extensions = schema?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(schema.Extensions) : null;
241-
Annotations = schema?.Annotations != null ? new Dictionary<string, object>(schema?.Annotations) : null;
242-
UnrecognizedKeywords = schema?.UnrecognizedKeywords != null ? new Dictionary<string, JsonNode>(schema?.UnrecognizedKeywords) : null;
191+
Utils.CheckArgumentNull(schema);
192+
Title = schema.Title ?? Title;
193+
Id = schema.Id ?? Id;
194+
Const = schema.Const ?? Const;
195+
Schema = schema.Schema ?? Schema;
196+
Comment = schema.Comment ?? Comment;
197+
Vocabulary = schema.Vocabulary != null ? new Dictionary<string, bool>(schema.Vocabulary) : null;
198+
DynamicAnchor = schema.DynamicAnchor ?? DynamicAnchor;
199+
DynamicRef = schema.DynamicRef ?? DynamicRef;
200+
Definitions = schema.Definitions != null ? new Dictionary<string, IOpenApiSchema>(schema.Definitions) : null;
201+
UnevaluatedProperties = schema.UnevaluatedProperties;
202+
V31ExclusiveMaximum = schema.V31ExclusiveMaximum ?? V31ExclusiveMaximum;
203+
V31ExclusiveMinimum = schema.V31ExclusiveMinimum ?? V31ExclusiveMinimum;
204+
Type = schema.Type ?? Type;
205+
Format = schema.Format ?? Format;
206+
Description = schema.Description ?? Description;
207+
Maximum = schema.Maximum ?? Maximum;
208+
ExclusiveMaximum = schema.ExclusiveMaximum ?? ExclusiveMaximum;
209+
Minimum = schema.Minimum ?? Minimum;
210+
ExclusiveMinimum = schema.ExclusiveMinimum ?? ExclusiveMinimum;
211+
MaxLength = schema.MaxLength ?? MaxLength;
212+
MinLength = schema.MinLength ?? MinLength;
213+
Pattern = schema.Pattern ?? Pattern;
214+
MultipleOf = schema.MultipleOf ?? MultipleOf;
215+
Default = schema.Default != null ? JsonNodeCloneHelper.Clone(schema.Default) : null;
216+
ReadOnly = schema.ReadOnly;
217+
WriteOnly = schema.WriteOnly;
218+
AllOf = schema.AllOf != null ? new List<IOpenApiSchema>(schema.AllOf) : null;
219+
OneOf = schema.OneOf != null ? new List<IOpenApiSchema>(schema.OneOf) : null;
220+
AnyOf = schema.AnyOf != null ? new List<IOpenApiSchema>(schema.AnyOf) : null;
221+
Not = schema.Not?.CreateShallowCopy();
222+
Required = schema.Required != null ? new HashSet<string>(schema.Required) : null;
223+
Items = schema.Items?.CreateShallowCopy();
224+
MaxItems = schema.MaxItems ?? MaxItems;
225+
MinItems = schema.MinItems ?? MinItems;
226+
UniqueItems = schema.UniqueItems ?? UniqueItems;
227+
Properties = schema.Properties != null ? new Dictionary<string, IOpenApiSchema>(schema.Properties) : null;
228+
PatternProperties = schema.PatternProperties != null ? new Dictionary<string, IOpenApiSchema>(schema.PatternProperties) : null;
229+
MaxProperties = schema.MaxProperties ?? MaxProperties;
230+
MinProperties = schema.MinProperties ?? MinProperties;
231+
AdditionalPropertiesAllowed = schema.AdditionalPropertiesAllowed;
232+
AdditionalProperties = schema.AdditionalProperties?.CreateShallowCopy();
233+
Discriminator = schema.Discriminator != null ? new(schema.Discriminator) : null;
234+
Example = schema.Example != null ? JsonNodeCloneHelper.Clone(schema.Example) : null;
235+
Examples = schema.Examples != null ? new List<JsonNode>(schema.Examples) : null;
236+
Enum = schema.Enum != null ? new List<JsonNode>(schema.Enum) : null;
237+
Nullable = schema.Nullable;
238+
ExternalDocs = schema.ExternalDocs != null ? new(schema.ExternalDocs) : null;
239+
Deprecated = schema.Deprecated;
240+
Xml = schema.Xml != null ? new(schema.Xml) : null;
241+
Extensions = schema.Extensions != null ? new Dictionary<string, IOpenApiExtension>(schema.Extensions) : null;
242+
Annotations = schema.Annotations != null ? new Dictionary<string, object>(schema.Annotations) : null;
243+
UnrecognizedKeywords = schema.UnrecognizedKeywords != null ? new Dictionary<string, JsonNode>(schema.UnrecognizedKeywords) : null;
243244
}
244245

245246
/// <inheritdoc />
@@ -736,5 +737,11 @@ private void DowncastTypeArrayToV2OrV3(JsonSchemaType schemaType, IOpenApiWriter
736737
}
737738
}
738739
}
740+
741+
/// <inheritdoc/>
742+
public IOpenApiSchema CreateShallowCopy()
743+
{
744+
return new OpenApiSchema(this);
745+
}
739746
}
740747
}

src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs

+7
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,12 @@ public override IOpenApiSchema CopyReferenceAsTargetElementWithOverrides(IOpenAp
193193
{
194194
return source is OpenApiSchema ? new OpenApiSchema(this) : source;
195195
}
196+
/// <inheritdoc/>
197+
public IOpenApiSchema CreateShallowCopy()
198+
{
199+
return _target is null ?
200+
new OpenApiSchemaReference(Reference.Id, Reference?.HostDocument, Reference?.ExternalResource) :
201+
new OpenApiSchemaReference(_target, Reference.Id);
202+
}
196203
}
197204
}

src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,13 @@ private static OpenApiRequestBody CreateFormBody(ParsingContext context, List<Op
154154
k => k.Name,
155155
v =>
156156
{
157-
var schema = new OpenApiSchema(v.Schema)
157+
var schema = v.Schema.CreateShallowCopy();
158+
schema.Description = v.Description;
159+
if (schema is OpenApiSchema openApiSchema)
158160
{
159-
Description = v.Description,
160-
Extensions = v.Extensions
161-
};
162-
return (IOpenApiSchema)schema;
161+
openApiSchema.Extensions = v.Extensions;
162+
}
163+
return schema;
163164
}),
164165
Required = new HashSet<string>(formParameters.Where(static p => p.Required).Select(static p => p.Name), StringComparer.Ordinal)
165166
}

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs

+4-6
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,11 @@ public void TestSchemaCopyConstructorWithTypeArrayWorks()
140140
};
141141

142142
// Act
143-
var schemaWithArrayCopy = new OpenApiSchema(schemaWithTypeArray);
143+
var schemaWithArrayCopy = schemaWithTypeArray.CreateShallowCopy() as OpenApiSchema;
144144
schemaWithArrayCopy.Type = JsonSchemaType.String;
145145

146-
var simpleSchemaCopy = new OpenApiSchema(simpleSchema)
147-
{
148-
Type = JsonSchemaType.String | JsonSchemaType.Null
149-
};
146+
var simpleSchemaCopy = simpleSchema.CreateShallowCopy() as OpenApiSchema;
147+
simpleSchemaCopy.Type = JsonSchemaType.String | JsonSchemaType.Null;
150148

151149
// Assert
152150
Assert.NotEqual(schemaWithTypeArray.Type, schemaWithArrayCopy.Type);
@@ -294,7 +292,7 @@ public void CloningSchemaWithExamplesAndEnumsShouldSucceed()
294292
Enum = [1, 2, 3]
295293
};
296294

297-
var clone = new OpenApiSchema(schema);
295+
var clone = schema.CreateShallowCopy() as OpenApiSchema;
298296
clone.Examples.Add(4);
299297
clone.Enum.Add(4);
300298
clone.Default = 6;

test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs

+5-7
Original file line numberDiff line numberDiff line change
@@ -471,10 +471,8 @@ public void OpenApiSchemaCopyConstructorSucceeds()
471471
Format = "date"
472472
};
473473

474-
var actualSchema = new OpenApiSchema(baseSchema)
475-
{
476-
Nullable = true
477-
};
474+
var actualSchema = baseSchema.CreateShallowCopy() as OpenApiSchema;
475+
actualSchema.Nullable = true;
478476

479477
Assert.Equal(JsonSchemaType.String, actualSchema.Type);
480478
Assert.Equal("date", actualSchema.Format);
@@ -493,7 +491,7 @@ public void OpenApiSchemaCopyConstructorWithAnnotationsSucceeds()
493491
}
494492
};
495493

496-
var actualSchema = new OpenApiSchema(baseSchema);
494+
var actualSchema = baseSchema.CreateShallowCopy();
497495

498496
Assert.Equal(baseSchema.Annotations["key1"], actualSchema.Annotations["key1"]);
499497

@@ -531,7 +529,7 @@ public void CloningSchemaExamplesWorks(JsonNode example)
531529
};
532530

533531
// Act && Assert
534-
var schemaCopy = new OpenApiSchema(schema);
532+
var schemaCopy = schema.CreateShallowCopy();
535533

536534
// Act && Assert
537535
schema.Example.Should().BeEquivalentTo(schemaCopy.Example, options => options
@@ -552,7 +550,7 @@ public void CloningSchemaExtensionsWorks()
552550
};
553551

554552
// Act && Assert
555-
var schemaCopy = new OpenApiSchema(schema);
553+
var schemaCopy = schema.CreateShallowCopy() as OpenApiSchema;
556554
Assert.Single(schemaCopy.Extensions);
557555

558556
// Act && Assert

0 commit comments

Comments
 (0)