Beispiel #1
0
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="NullSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="NullSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is NullSchema)
            {
                var tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                return(JsonDeserializerBuilderCaseResult.FromExpression(
                           Expression.Block(
                               Expression.IfThen(
                                   Expression.NotEqual(
                                       Expression.Property(context.Reader, tokenType),
                                       Expression.Constant(JsonTokenType.Null)),
                                   Expression.Throw(
                                       Expression.Call(
                                           null,
                                           getUnexpectedTokenException,
                                           context.Reader,
                                           Expression.Constant(new[] { JsonTokenType.Null })))),
                               Expression.Default(type))));
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonNullDeserializerBuilderCase)} can only be applied to {nameof(NullSchema)}s.")));
            }
        }
        /// <summary>
        /// Creates a new <see cref="JsonDeserializerBuilderCaseResult" /> for an unsuccessful
        /// outcome.
        /// </summary>
        /// <param name="exception">
        /// An exception describing the inapplicability of the case.
        /// </param>
        /// <returns>
        /// A <see cref="JsonDeserializerBuilderCaseResult" /> with <see cref="Exceptions" />
        /// populated and <see cref="Expression" /> <c>null</c>.
        /// </returns>
        public static JsonDeserializerBuilderCaseResult FromException(Exception exception)
        {
            var result = new JsonDeserializerBuilderCaseResult();

            result.Exceptions.Add(exception);

            return(result);
        }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="TimestampLogicalType" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// has a <see cref="TimestampLogicalType" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedSchemaException">
        /// Thrown when <paramref name="schema" /> is not a <see cref="LongSchema" /> or when
        /// <paramref name="schema" /> does not have a <see cref="MicrosecondTimestampLogicalType" />
        /// or a <see cref="MillisecondTimestampLogicalType" />.
        /// </exception>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <see cref="DateTime" /> cannot be converted to <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema.LogicalType is TimestampLogicalType)
            {
                if (schema is not LongSchema)
                {
                    throw new UnsupportedSchemaException(schema);
                }

                Expression epoch = Expression.Constant(Epoch);
                Expression factor;

                if (schema.LogicalType is MicrosecondTimestampLogicalType)
                {
                    factor = Expression.Constant(TimeSpan.TicksPerMillisecond / 1000);
                }
                else if (schema.LogicalType is MillisecondTimestampLogicalType)
                {
                    factor = Expression.Constant(TimeSpan.TicksPerMillisecond);
                }
                else
                {
                    throw new UnsupportedSchemaException(schema);
                }

                var getInt64 = typeof(Utf8JsonReader)
                               .GetMethod(nameof(Utf8JsonReader.GetInt64), Type.EmptyTypes);

                Expression expression = Expression.Call(context.Reader, getInt64);

                var addTicks = typeof(DateTime)
                               .GetMethod(nameof(DateTime.AddTicks));

                try
                {
                    // return Epoch.AddTicks(value * factor);
                    return(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(
                                   Expression.Call(epoch, addTicks, Expression.Multiply(expression, factor)),
                                   type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonTimestampDeserializerBuilderCase)} can only be applied schemas with a {nameof(TimestampLogicalType)}.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="BytesSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="BytesSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <see cref="T:System.Byte[]" /> cannot be converted to <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is BytesSchema bytesSchema)
            {
                var tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                var getBytes = typeof(Encoding)
                               .GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) });

                try
                {
                    return(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(
                                   Expression.Block(
                                       Expression.IfThen(
                                           Expression.NotEqual(
                                               Expression.Property(context.Reader, tokenType),
                                               Expression.Constant(JsonTokenType.String)),
                                           Expression.Throw(
                                               Expression.Call(
                                                   null,
                                                   getUnexpectedTokenException,
                                                   context.Reader,
                                                   Expression.Constant(new[] { JsonTokenType.String })))),
                                       Expression.Call(
                                           Expression.Constant(JsonEncoding.Bytes),
                                           getBytes,
                                           Expression.Call(context.Reader, getString))),
                                   type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {bytesSchema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonBytesDeserializerBuilderCase)} can only be applied to {nameof(BytesSchema)}s.")));
            }
        }
Beispiel #5
0
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="LongSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="LongSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <see cref="long" /> cannot be converted to <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is LongSchema longSchema)
            {
                var getInt64 = typeof(Utf8JsonReader)
                               .GetMethod(nameof(Utf8JsonReader.GetInt64), Type.EmptyTypes);

                try
                {
                    return(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(Expression.Call(context.Reader, getInt64), type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {longSchema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonLongDeserializerBuilderCase)} can only be applied to {nameof(LongSchema)}s.")));
            }
        }
Beispiel #6
0
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="DecimalLogicalType" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// has a <see cref="DecimalLogicalType" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedSchemaException">
        /// Thrown when <paramref name="schema" /> is not a <see cref="BytesSchema" /> or a
        /// <see cref="FixedSchema "/>.
        /// </exception>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <see cref="decimal" /> cannot be converted to <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema.LogicalType is DecimalLogicalType decimalLogicalType)
            {
                var precision = decimalLogicalType.Precision;
                var scale     = decimalLogicalType.Scale;

                var bytes = Expression.Variable(typeof(byte[]));

                var tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                var getBytes = typeof(Encoding)
                               .GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) });

                var getUnexpectedSizeException = typeof(JsonExceptionHelper)
                                                 .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedSizeException));

                Expression expression;

                if (schema is BytesSchema)
                {
                    expression = Expression.Block(
                        Expression.IfThen(
                            Expression.NotEqual(
                                Expression.Property(context.Reader, tokenType),
                                Expression.Constant(JsonTokenType.String)),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnexpectedTokenException,
                                    context.Reader,
                                    Expression.Constant(new[] { JsonTokenType.String })))),
                        Expression.Assign(
                            bytes,
                            Expression.Call(
                                Expression.Constant(JsonEncoding.Bytes),
                                getBytes,
                                Expression.Call(context.Reader, getString))),
                        bytes);
                }
                else if (schema is FixedSchema fixedSchema)
                {
                    expression = Expression.Block(
                        Expression.IfThen(
                            Expression.NotEqual(
                                Expression.Property(context.Reader, tokenType),
                                Expression.Constant(JsonTokenType.String)),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnexpectedTokenException,
                                    context.Reader,
                                    Expression.Constant(new[] { JsonTokenType.String })))),
                        Expression.Assign(
                            bytes,
                            Expression.Call(
                                Expression.Constant(JsonEncoding.Bytes),
                                getBytes,
                                Expression.Call(context.Reader, getString))),
                        Expression.IfThen(
                            Expression.NotEqual(
                                Expression.ArrayLength(bytes),
                                Expression.Constant(fixedSchema.Size)),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnexpectedSizeException,
                                    context.Reader,
                                    Expression.Constant(fixedSchema.Size),
                                    Expression.ArrayLength(bytes)))),
                        bytes);
                }
                else
                {
                    throw new UnsupportedSchemaException(schema);
                }

                // declare variables for in-place transformation:
                var remainder = Expression.Variable(typeof(BigInteger));

                var divide = typeof(BigInteger)
                             .GetMethod(nameof(BigInteger.DivRem), new[] { typeof(BigInteger), typeof(BigInteger), typeof(BigInteger).MakeByRefType() });

                var integerConstructor = typeof(BigInteger)
                                         .GetConstructor(new[] { typeof(byte[]) });

                var reverse = typeof(Array)
                              .GetMethod(nameof(Array.Reverse), new[] { typeof(Array) });

                // var bytes = ...;
                //
                // // BigInteger is little-endian, so reverse:
                // Array.Reverse(bytes);
                //
                // var whole = BigInteger.DivRem(new BigInteger(bytes), BigInteger.Pow(10, scale), out var remainder);
                // var fraction = (decimal)remainder / (decimal)Math.Pow(10, scale);
                //
                // return whole + fraction;
                expression = Expression.Block(
                    new[] { bytes, remainder },
                    Expression.Assign(bytes, expression),
                    Expression.Call(null, reverse, bytes),
                    Expression.Add(
                        Expression.ConvertChecked(
                            Expression.Call(
                                null,
                                divide,
                                Expression.New(integerConstructor, bytes),
                                Expression.Constant(BigInteger.Pow(10, scale)),
                                remainder),
                            typeof(decimal)),
                        Expression.Divide(
                            Expression.ConvertChecked(remainder, typeof(decimal)),
                            Expression.Constant((decimal)Math.Pow(10, scale)))));

                try
                {
                    return(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(expression, type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonDecimalDeserializerBuilderCase)} can only be applied schemas with a {nameof(DecimalLogicalType)}.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for an <see cref="ArraySchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="type" />
        /// is an enumerable type and <paramref name="schema" /> is an <see cref="ArraySchema" />;
        /// an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> is not assignable from any supported array or
        /// collection <see cref="Type" /> and does not have a constructor that can be used to
        /// instantiate it.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is ArraySchema arraySchema)
            {
                var enumerableType = GetEnumerableType(type);

                if (enumerableType is not null || type == typeof(object))
                {
                    // support dynamic mapping:
                    var itemType = enumerableType ?? typeof(object);

                    var instantiateCollection = BuildIntermediateCollection(type, itemType);

                    var readItem = DeserializerBuilder
                                   .BuildExpression(itemType, arraySchema.Item, context);

                    var collection = Expression.Parameter(instantiateCollection.Type);
                    var loop       = Expression.Label();

                    var tokenType = typeof(Utf8JsonReader)
                                    .GetProperty(nameof(Utf8JsonReader.TokenType));

                    var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                      .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                    var read = typeof(Utf8JsonReader)
                               .GetMethod(nameof(Utf8JsonReader.Read), Type.EmptyTypes);

                    var add = collection.Type.GetMethod("Add", new[] { readItem.Type });

                    Expression expression = Expression.Block(
                        new[] { collection },
                        Expression.IfThen(
                            Expression.NotEqual(
                                Expression.Property(context.Reader, tokenType),
                                Expression.Constant(JsonTokenType.StartArray)),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnexpectedTokenException,
                                    context.Reader,
                                    Expression.Constant(new[] { JsonTokenType.StartArray })))),
                        Expression.Assign(collection, instantiateCollection),
                        Expression.Loop(
                            Expression.Block(
                                Expression.Call(context.Reader, read),
                                Expression.IfThen(
                                    Expression.Equal(
                                        Expression.Property(context.Reader, tokenType),
                                        Expression.Constant(JsonTokenType.EndArray)),
                                    Expression.Break(loop)),
                                Expression.Call(collection, add, readItem)),
                            loop),
                        collection);

                    if (!type.IsAssignableFrom(expression.Type) && GetCollectionConstructor(type) is ConstructorInfo constructor)
                    {
                        expression = Expression.New(constructor, expression);
                    }

                    try
                    {
                        return(JsonDeserializerBuilderCaseResult.FromExpression(
                                   BuildConversion(expression, type)));
                    }
                    catch (InvalidOperationException exception)
                    {
                        throw new UnsupportedTypeException(type, $"Failed to map {arraySchema} to {type}.", exception);
                    }
                }
                else
                {
                    return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonArrayDeserializerBuilderCase)} can only be applied to enumerable types.")));
                }
            }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="DurationLogicalType" />.
        /// </summary>
        /// <returns>
        /// A successful result if <paramref name="type" /> the schema’s logical type is a
        /// <see cref="DurationLogicalType" />; an unsuccessful result otherwise.
        /// </returns>
        /// <exception cref="UnsupportedSchemaException">
        /// Thrown when the schema is not a <see cref="FixedSchema" /> with size 12 and logical
        /// type <see cref="DurationLogicalType" />.
        /// </exception>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <see cref="TimeSpan" /> cannot be converted to <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema.LogicalType is DurationLogicalType)
            {
                if (!(schema is FixedSchema fixedSchema && fixedSchema.Size == 12))
                {
                    throw new UnsupportedSchemaException(schema);
                }

                var bytes = Expression.Parameter(typeof(byte[]));

                var copy = typeof(Array)
                           .GetMethod(nameof(Array.Copy), new[] { typeof(Array), typeof(int), typeof(Array), typeof(int), typeof(int) });

                var tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                var getBytes = typeof(Encoding)
                               .GetMethod(nameof(Encoding.GetBytes), new[] { typeof(string) });

                var getUnexpectedSizeException = typeof(JsonExceptionHelper)
                                                 .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedSizeException));

                var reverse = typeof(Array)
                              .GetMethod(nameof(Array.Reverse), new[] { typeof(Array) });

                var toUInt32 = typeof(BitConverter)
                               .GetMethod(nameof(BitConverter.ToUInt32), new[] { typeof(byte[]), typeof(int) });

                Expression Read(Expression offset)
                {
                    var component = Expression.Variable(typeof(byte[]));

                    var expressions = new List <Expression>
                    {
                        Expression.Assign(
                            component,
                            Expression.NewArrayBounds(typeof(byte), Expression.Constant(4))),
                        Expression.Call(null, copy, bytes, offset, component, Expression.Constant(0), Expression.ArrayLength(component)),
                    };

                    if (!BitConverter.IsLittleEndian)
                    {
                        expressions.Add(Expression.Call(null, reverse, Expression.Convert(component, typeof(Array))));
                    }

                    expressions.Add(component);

                    return(Expression.ConvertChecked(
                               Expression.Call(
                                   null,
                                   toUInt32,
                                   Expression.Block(
                                       new[] { component },
                                       expressions),
                                   Expression.Constant(0)),
                               typeof(long)));
                }

                var exceptionConstructor = typeof(OverflowException)
                                           .GetConstructor(new[] { typeof(string) });

                var timeSpanConstructor = typeof(TimeSpan)
                                          .GetConstructor(new[] { typeof(long) });

                try
                {
                    return(JsonDeserializerBuilderCaseResult.FromExpression(
                               BuildConversion(
                                   Expression.Block(
                                       new[] { bytes },
                                       Expression.IfThen(
                                           Expression.NotEqual(
                                               Expression.Property(context.Reader, tokenType),
                                               Expression.Constant(JsonTokenType.String)),
                                           Expression.Throw(
                                               Expression.Call(
                                                   null,
                                                   getUnexpectedTokenException,
                                                   context.Reader,
                                                   Expression.Constant(new[] { JsonTokenType.String })))),
                                       Expression.Assign(
                                           bytes,
                                           Expression.Call(
                                               Expression.Constant(JsonEncoding.Bytes),
                                               getBytes,
                                               Expression.Call(context.Reader, getString))),
                                       Expression.IfThen(
                                           Expression.NotEqual(
                                               Expression.ArrayLength(bytes),
                                               Expression.Constant(fixedSchema.Size)),
                                           Expression.Throw(
                                               Expression.Call(
                                                   null,
                                                   getUnexpectedSizeException,
                                                   context.Reader,
                                                   Expression.Constant(fixedSchema.Size),
                                                   Expression.ArrayLength(bytes)))),
                                       Expression.IfThen(
                                           Expression.NotEqual(Read(Expression.Constant(0)), Expression.Constant(0L)),
                                           Expression.Throw(
                                               Expression.New(
                                                   exceptionConstructor,
                                                   Expression.Constant("Durations containing months cannot be accurately deserialized to a TimeSpan.")))),
                                       Expression.New(
                                           timeSpanConstructor,
                                           Expression.AddChecked(
                                               Expression.MultiplyChecked(Read(Expression.Constant(4)), Expression.Constant(TimeSpan.TicksPerDay)),
                                               Expression.MultiplyChecked(Read(Expression.Constant(8)), Expression.Constant(TimeSpan.TicksPerMillisecond))))),
                                   type)));
                }
                catch (InvalidOperationException exception)
                {
                    throw new UnsupportedTypeException(type, $"Failed to map {schema} to {type}.", exception);
                }
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonDurationDeserializerBuilderCase)} can only be applied schemas with a {nameof(DurationLogicalType)}.")));
            }
        }
Beispiel #9
0
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="MapSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="type" />
        /// is a dictionary type and <paramref name="schema" /> is a <see cref="MapSchema" />; an
        /// unsuccessful <see cref="JsonDeserializerBuilderCaseResult" /> otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> is not assignable from any supported dictionary
        /// <see cref="Type" /> and does not have a constructor that can be used to instantiate it.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is MapSchema mapSchema)
            {
                var dictionaryTypes = GetDictionaryTypes(type);

                if (dictionaryTypes is not null || type == typeof(object))
                {
                    // support dynamic mapping:
                    var keyType   = dictionaryTypes?.Key ?? typeof(string);
                    var valueType = dictionaryTypes?.Value ?? typeof(object);

                    var instantiateDictionary = BuildIntermediateDictionary(type, keyType, valueType);

                    var readKey = DeserializerBuilder
                                  .BuildExpression(keyType, new StringSchema(), context);

                    var readValue = DeserializerBuilder
                                    .BuildExpression(valueType, mapSchema.Value, context);

                    var dictionary = Expression.Parameter(instantiateDictionary.Type);
                    var key        = Expression.Parameter(readKey.Type);
                    var loop       = Expression.Label();

                    var tokenType = typeof(Utf8JsonReader)
                                    .GetProperty(nameof(Utf8JsonReader.TokenType));

                    var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                      .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                    var read = typeof(Utf8JsonReader)
                               .GetMethod(nameof(Utf8JsonReader.Read), Type.EmptyTypes);

                    var add = dictionary.Type.GetMethod("Add", new[] { readKey.Type, readValue.Type });

                    Expression expression = Expression.Block(
                        new[] { dictionary },
                        Expression.IfThen(
                            Expression.NotEqual(
                                Expression.Property(context.Reader, tokenType),
                                Expression.Constant(JsonTokenType.StartObject)),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnexpectedTokenException,
                                    context.Reader,
                                    Expression.Constant(new[] { JsonTokenType.StartObject })))),
                        Expression.Assign(dictionary, instantiateDictionary),
                        Expression.Loop(
                            Expression.Block(
                                new[] { key },
                                Expression.Call(context.Reader, read),
                                Expression.IfThen(
                                    Expression.Equal(
                                        Expression.Property(context.Reader, tokenType),
                                        Expression.Constant(JsonTokenType.EndObject)),
                                    Expression.Break(loop)),
                                Expression.Assign(key, readKey),
                                Expression.Call(context.Reader, read),
                                Expression.Call(dictionary, add, key, readValue)),
                            loop),
                        dictionary);

                    if (!type.IsAssignableFrom(expression.Type) && GetDictionaryConstructor(type) is ConstructorInfo constructor)
                    {
                        expression = Expression.New(constructor, expression);
                    }

                    try
                    {
                        return(JsonDeserializerBuilderCaseResult.FromExpression(
                                   BuildConversion(expression, type)));
                    }
                    catch (InvalidOperationException exception)
                    {
                        throw new UnsupportedTypeException(type, $"Failed to map {mapSchema} to {type}.", exception);
                    }
                }
                else
                {
                    return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedTypeException(type, $"{nameof(JsonMapDeserializerBuilderCase)} can only be applied to dictionary types.")));
                }
            }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for an <see cref="EnumSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is an <see cref="EnumSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> is an enum type without a matching member for each
        /// symbol in <paramref name="schema" /> or when <see cref="string" /> cannot be converted
        /// to <paramref name="type" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is EnumSchema enumSchema)
            {
                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                Expression expression = Expression.Call(context.Reader, getString);

                var underlying = Nullable.GetUnderlyingType(type) ?? type;

                // enum fields will always be public static, so no need to expose binding flags:
                var fields = underlying.GetFields(BindingFlags.Public | BindingFlags.Static);

                // find a match for each enum in the schema:
                var cases = underlying.IsEnum
                    ? enumSchema.Symbols
                            .Select(symbol =>
                {
                    var match = fields.SingleOrDefault(field => IsMatch(symbol, field));

                    if (enumSchema.Default != null)
                    {
                        match ??= fields.SingleOrDefault(field => IsMatch(enumSchema.Default, field));
                    }

                    if (match == null)
                    {
                        throw new UnsupportedTypeException(type, $"{type} has no value that matches {symbol} and no default value is defined.");
                    }

                    return(Expression.SwitchCase(
                               BuildConversion(Expression.Constant(Enum.Parse(underlying, match.Name)), type),
                               Expression.Constant(symbol)));
                })
                    : enumSchema.Symbols
                            .Select(symbol =>
                {
                    return(Expression.SwitchCase(
                               BuildConversion(Expression.Constant(symbol), type),
                               Expression.Constant(symbol)));
                });

                var position = typeof(Utf8JsonReader)
                               .GetProperty(nameof(Utf8JsonReader.TokenStartIndex))
                               .GetGetMethod();

                var exceptionConstructor = typeof(InvalidEncodingException)
                                           .GetConstructor(new[] { typeof(long), typeof(string), typeof(Exception) });

                // generate a switch on the symbol:
                return(JsonDeserializerBuilderCaseResult.FromExpression(
                           Expression.Switch(
                               expression,
                               Expression.Throw(
                                   Expression.New(
                                       exceptionConstructor,
                                       Expression.Property(context.Reader, position),
                                       Expression.Constant($"Invalid enum symbol."),
                                       Expression.Constant(null, typeof(Exception))),
                                   type),
                               cases.ToArray())));
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonEnumDeserializerBuilderCase)} can only be applied to {nameof(EnumSchema)}s.")));
            }
        }
        /// <summary>
        /// Builds a <see cref="JsonDeserializer{T}" /> for a <see cref="UnionSchema" />.
        /// </summary>
        /// <returns>
        /// A successful <see cref="JsonDeserializerBuilderCaseResult" /> if <paramref name="schema" />
        /// is a <see cref="UnionSchema" />; an unsuccessful <see cref="JsonDeserializerBuilderCaseResult" />
        /// otherwise.
        /// </returns>
        /// <exception cref="UnsupportedSchemaException">
        /// Thrown when <paramref name="schema" /> has no <see cref="UnionSchema.Schemas" />.
        /// </exception>
        /// <exception cref="UnsupportedTypeException">
        /// Thrown when <paramref name="type" /> cannot be mapped to each <see cref="Schema" /> in
        /// <paramref name="schema" />.
        /// </exception>
        /// <inheritdoc />
        public virtual JsonDeserializerBuilderCaseResult BuildExpression(Type type, Schema schema, JsonDeserializerBuilderContext context)
        {
            if (schema is UnionSchema unionSchema)
            {
                if (unionSchema.Schemas.Count < 1)
                {
                    throw new UnsupportedSchemaException(schema, "A deserializer cannot be built for an empty union.");
                }

                var tokenType = typeof(Utf8JsonReader)
                                .GetProperty(nameof(Utf8JsonReader.TokenType));

                var getUnexpectedTokenException = typeof(JsonExceptionHelper)
                                                  .GetMethod(nameof(JsonExceptionHelper.GetUnexpectedTokenException));

                var read = typeof(Utf8JsonReader)
                           .GetMethod(nameof(Utf8JsonReader.Read), Type.EmptyTypes);

                var getString = typeof(Utf8JsonReader)
                                .GetMethod(nameof(Utf8JsonReader.GetString), Type.EmptyTypes);

                var getUnknownUnionMemberException = typeof(JsonExceptionHelper)
                                                     .GetMethod(nameof(JsonExceptionHelper.GetUnknownUnionMemberException));

                var schemas    = unionSchema.Schemas.ToList();
                var candidates = schemas.Where(s => s is not NullSchema).ToList();
                var @null      = schemas.Find(s => s is NullSchema);

                var cases = candidates.Select(child =>
                {
                    var selected = SelectType(type, child);

                    return(Expression.SwitchCase(
                               BuildConversion(
                                   Expression.Block(
                                       Expression.Call(context.Reader, read),
                                       DeserializerBuilder.BuildExpression(selected, child, context)),
                                   type),
                               Expression.Constant(GetSchemaName(child))));
                }).ToArray();

                var value = Expression.Parameter(type);

                Expression expression = Expression.Block(
                    new[] { value },
                    Expression.IfThen(
                        Expression.NotEqual(
                            Expression.Property(context.Reader, tokenType),
                            Expression.Constant(JsonTokenType.StartObject)),
                        Expression.Throw(
                            Expression.Call(
                                null,
                                getUnexpectedTokenException,
                                context.Reader,
                                Expression.Constant(new[] { JsonTokenType.StartObject })))),
                    Expression.Call(context.Reader, read),
                    Expression.Assign(
                        value,
                        Expression.Switch(
                            Expression.Call(context.Reader, getString),
                            Expression.Throw(
                                Expression.Call(
                                    null,
                                    getUnknownUnionMemberException,
                                    context.Reader),
                                type),
                            cases)),
                    Expression.Call(context.Reader, read),
                    Expression.IfThen(
                        Expression.NotEqual(
                            Expression.Property(context.Reader, tokenType),
                            Expression.Constant(JsonTokenType.EndObject)),
                        Expression.Throw(
                            Expression.Call(
                                null,
                                getUnexpectedTokenException,
                                context.Reader,
                                Expression.Constant(new[] { JsonTokenType.EndObject })))),
                    value);

                if (@null != null)
                {
                    var selected   = SelectType(type, @null);
                    var underlying = Nullable.GetUnderlyingType(selected);

                    if (selected.IsValueType && underlying == null)
                    {
                        throw new UnsupportedTypeException(type, $"A deserializer for a union containing {typeof(NullSchema)} cannot be built for {selected}.");
                    }

                    expression = Expression.Condition(
                        Expression.Equal(
                            Expression.Property(context.Reader, tokenType),
                            Expression.Constant(JsonTokenType.Null)),
                        BuildConversion(
                            DeserializerBuilder.BuildExpression(selected, @null, context),
                            type),
                        expression);
                }

                return(JsonDeserializerBuilderCaseResult.FromExpression(expression));
            }
            else
            {
                return(JsonDeserializerBuilderCaseResult.FromException(new UnsupportedSchemaException(schema, $"{nameof(JsonUnionDeserializerBuilderCase)} can only be applied to {nameof(UnionSchema)}s.")));
            }
        }