Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -284,18 +284,20 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right)
left._typeInfoResolver == right._typeInfoResolver &&
CompareLists(left._converters, right._converters);

static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationList<TValue> right)
static bool CompareLists<TValue>(ConfigurationList<TValue>? left, ConfigurationList<TValue>? right)
where TValue : class?
{
int n;
if ((n = left.Count) != right.Count)
int leftCount = left is null ? 0 : left.Count;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious - when converters are not null - what's the chance that someone cached converter but didn't cache options? Won't this converters check realistically always be false?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly we could add equals and hash code to our implementations of converters and make them return trues when types match? That would make it relatively more likely to be true

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this converters check realistically always be false?

It would almost always be false, I wouldn't expect users to cache converters if they're not caching options.

Possibly we could add equals and hash code to our implementations of converters and make them return trues when types match? That would make it relatively more likely to be true

It might also be false because some converters accept parameters. We want to avoid false positives under all circumstances since it would lead to observable bugs -- on the other hand we tolerate false negatives since they can only result in reduced performance.

int rightCount = right is null ? 0 : right.Count;

if (leftCount != rightCount)
{
return false;
}

for (int i = 0; i < n; i++)
for (int i = 0; i < leftCount; i++)
{
if (left[i] != right[i])
if (left![i] != right![i])
{
return false;
}
Expand Down Expand Up @@ -331,12 +333,15 @@ public int GetHashCode(JsonSerializerOptions options)

return hc.ToHashCode();

static void AddListHashCode<TValue>(ref HashCode hc, ConfigurationList<TValue> list)
static void AddListHashCode<TValue>(ref HashCode hc, ConfigurationList<TValue>? list)
{
int n = list.Count;
for (int i = 0; i < n; i++)
if (list != null)
{
AddHashCode(ref hc, list[i]);
int n = list.Count;
for (int i = 0; i < n; i++)
{
AddHashCode(ref hc, list[i]);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public sealed partial class JsonSerializerOptions
/// <remarks>
/// Once serialization or deserialization occurs, the list cannot be modified.
/// </remarks>
public IList<JsonConverter> Converters => _converters;
public IList<JsonConverter> Converters => _converters ??= new ConverterList(this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't thread-safe, in that two threads concurrently accessing this could end up getting different ConverterList instances. Is that ok?

And, it seems like this could transition from null to non-null even after being marked as isreadonly. Is that ok?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok for both cases, all logic consuming that field (including equality comparison in the global cache) has been updated so that null instances are equated to empty instances.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can, of course, result in issues when multiple threads attempt to configure the same mutable instance, but generally speaking the type is not designed to be thread safe while mutable.


/// <summary>
/// Returns the converter for the specified type.
Expand Down Expand Up @@ -66,11 +66,14 @@ internal JsonConverter GetConverterInternal(Type typeToConvert)

internal JsonConverter? GetConverterFromList(Type typeToConvert)
{
foreach (JsonConverter item in _converters)
if (_converters != null)
{
if (item.CanConvert(typeToConvert))
foreach (JsonConverter item in _converters)
{
return item;
if (item.CanConvert(typeToConvert))
{
return item;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static JsonSerializerOptions Default
private JsonCommentHandling _readCommentHandling;
private ReferenceHandler? _referenceHandler;
private JavaScriptEncoder? _encoder;
private ConfigurationList<JsonConverter> _converters;
private ConfigurationList<JsonConverter>? _converters;
private JsonIgnoreCondition _defaultIgnoreCondition;
private JsonNumberHandling _numberHandling;
private JsonUnknownTypeHandling _unknownTypeHandling;
Expand All @@ -80,7 +80,6 @@ public static JsonSerializerOptions Default
/// </summary>
public JsonSerializerOptions()
{
_converters = new ConverterList(this);
TrackOptionsInstance(this);
}

Expand All @@ -102,7 +101,7 @@ public JsonSerializerOptions(JsonSerializerOptions options)
_jsonPropertyNamingPolicy = options._jsonPropertyNamingPolicy;
_readCommentHandling = options._readCommentHandling;
_referenceHandler = options._referenceHandler;
_converters = new ConverterList(this, options._converters);
_converters = options._converters is null ? null : new ConverterList(this, options._converters);
_encoder = options._encoder;
_defaultIgnoreCondition = options._defaultIgnoreCondition;
_numberHandling = options._numberHandling;
Expand All @@ -121,9 +120,6 @@ public JsonSerializerOptions(JsonSerializerOptions options)
EffectiveMaxDepth = options.EffectiveMaxDepth;
ReferenceHandlingStrategy = options.ReferenceHandlingStrategy;

// _cachingContext is not copied as sharing the JsonTypeInfo and JsonPropertyInfo caches can result in
// unnecessary references to type metadata, potentially hindering garbage collection on the source options.

TrackOptionsInstance(this);
}

Expand Down