diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FromFormOrJsonModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FromFormOrJsonModelBinder.cs
new file mode 100644
index 000000000000..4919138b4d2e
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FromFormOrJsonModelBinder.cs
@@ -0,0 +1,83 @@
+using System.Text.Json;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+
+///
+/// A generic ModelBinder that supports both 'application/json' and 'multipart/form-data' content types.
+/// For multipart requests, it expects a 'payload' field containing a JSON object.
+/// Optionally, the target model can implement to receive uploaded files from the form.
+///
+public class FromFormOrJsonModelBinder : IModelBinder where T : class
+{
+ private static readonly JsonSerializerOptions _defaultJsonOptions = new() { PropertyNameCaseInsensitive = true };
+
+ public async Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ var request = bindingContext.HttpContext.Request;
+ var modelName = bindingContext.ModelName;
+
+ if (string.IsNullOrEmpty(request.ContentType))
+ {
+ bindingContext.ModelState.AddModelError(modelName, "Missing Content-Type header.");
+ bindingContext.Result = ModelBindingResult.Failed();
+ return;
+ }
+
+ try
+ {
+ T? model = null;
+
+ if (request.ContentType.StartsWith("multipart/form-data", StringComparison.OrdinalIgnoreCase))
+ {
+ var form = await request.ReadFormAsync(bindingContext.HttpContext.RequestAborted);
+
+ if (form.TryGetValue("payload", out var payloadJson) && !StringValues.IsNullOrEmpty(payloadJson))
+ {
+ model = JsonSerializer.Deserialize(payloadJson.ToString(), _defaultJsonOptions);
+ }
+
+ if (model is IFormFileReceiver receiver)
+ {
+ receiver.SetFiles(form.Files);
+ }
+ }
+ else if (request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
+ {
+ model = await JsonSerializer.DeserializeAsync(
+ request.Body,
+ _defaultJsonOptions,
+ bindingContext.HttpContext.RequestAborted
+ );
+ }
+ else
+ {
+ bindingContext.ModelState.AddModelError(modelName, $"Unsupported Content-Type '{request.ContentType}'.");
+ bindingContext.Result = ModelBindingResult.Failed();
+ return;
+ }
+
+ if (model is null)
+ {
+ bindingContext.ModelState.AddModelError(modelName, "Could not deserialize the request body.");
+ bindingContext.Result = ModelBindingResult.Failed();
+ return;
+ }
+
+ bindingContext.Result = ModelBindingResult.Success(model);
+ }
+ catch (JsonException)
+ {
+ bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Invalid JSON payload.");
+ bindingContext.Result = ModelBindingResult.Failed();
+ }
+ catch (Exception)
+ {
+ bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Unexpected error during model binding.");
+ bindingContext.Result = ModelBindingResult.Failed();
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FromFormOrJsonModelBinderProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FromFormOrJsonModelBinderProvider.cs
new file mode 100644
index 000000000000..d25dfbb6e7e1
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FromFormOrJsonModelBinderProvider.cs
@@ -0,0 +1,32 @@
+using System;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using System.Linq;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
+{
+ ///
+ /// Provides a model binder for parameters annotated with FromFormOrJsonAttribute.
+ ///
+ public class FromFormOrJsonModelBinderProvider : IModelBinderProvider
+ {
+ public IModelBinder? GetBinder(ModelBinderProviderContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var parameter = context.Metadata.ParameterInfo;
+ var binderAttribute = parameter?.GetCustomAttributes(typeof(FromFormOrJsonAttribute), true)
+ .FirstOrDefault() as FromFormOrJsonAttribute;
+
+ if (binderAttribute != null)
+ {
+ var binderType = typeof(FromFormOrJsonModelBinder<>).MakeGenericType(context.Metadata.ModelType);
+ return (IModelBinder?)Activator.CreateInstance(binderType);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FromFormOrJsonAttribute.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FromFormOrJsonAttribute.cs
new file mode 100644
index 000000000000..90bb90a6b0b8
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/FromFormOrJsonAttribute.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Microsoft.AspNetCore.Mvc;
+
+///
+/// Indicates that a parameter should be bound using the FromFormOrJsonModelBinder.
+///
+[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
+public sealed class FromFormOrJsonAttribute : Attribute, IBindingSourceMetadata
+{
+ public BindingSource BindingSource => BindingSource.Custom;
+}
diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/IFormFileReceiver.cs b/src/Mvc/Mvc.Core/src/ModelBinding/IFormFileReceiver.cs
new file mode 100644
index 000000000000..7319c5685aaa
--- /dev/null
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/IFormFileReceiver.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding;
+
+///
+/// Optional interface for models that wish to receive uploaded files from multipart/form-data requests.
+///
+public interface IFormFileReceiver
+{
+ void SetFiles(IFormFileCollection files);
+}
diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/FromFormOrJsonModelBinderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/FromFormOrJsonModelBinderTest.cs
new file mode 100644
index 000000000000..50b6a930309a
--- /dev/null
+++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/FromFormOrJsonModelBinderTest.cs
@@ -0,0 +1,87 @@
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding.Tests;
+
+public class FromFormOrJsonModelBinderTest
+{
+ private static DefaultHttpContext CreateHttpContextWithJson(T payload)
+ {
+ var context = new DefaultHttpContext();
+ context.Request.ContentType = "application/json";
+
+ var json = JsonSerializer.Serialize(payload);
+ var bytes = Encoding.UTF8.GetBytes(json);
+ context.Request.Body = new MemoryStream(bytes);
+
+ return context;
+ }
+
+ private static DefaultHttpContext CreateHttpContextWithForm(T payload)
+ {
+ var context = new DefaultHttpContext();
+ context.Request.ContentType = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW";
+ var form = new FormCollection(new Dictionary
+ {
+ { "payload", JsonSerializer.Serialize(payload) }
+ });
+
+ context.Request.Form = form;
+ return context;
+ }
+
+ public class SampleDto
+ {
+ public string Name { get; set; }
+ }
+
+ [Fact]
+ public async Task BindsFromJsonPayload()
+ {
+ // Arrange
+ var dto = new SampleDto { Name = "Json Test" };
+ var httpContext = CreateHttpContextWithJson(dto);
+ var bindingContext = GetBindingContext(httpContext);
+
+ var binder = new FromFormOrJsonModelBinder();
+
+ // Act
+ await binder.BindModelAsync(bindingContext);
+
+ // Assert
+ Assert.True(bindingContext.Result.IsModelSet);
+ var result = Assert.IsType(bindingContext.Result.Model);
+ Assert.Equal("Json Test", result.Name);
+ }
+
+ private static DefaultModelBindingContext GetBindingContext(HttpContext httpContext)
+ {
+ var metadataProvider = new EmptyModelMetadataProvider();
+ var modelMetadata = metadataProvider.GetMetadataForType(typeof(T));
+ return new DefaultModelBindingContext
+ {
+ ModelMetadata = modelMetadata,
+ ModelName = "model",
+ ModelState = new ModelStateDictionary(),
+ ValueProvider = new SimpleValueProvider(),
+ ActionContext = new ActionContext
+ {
+ HttpContext = httpContext,
+ RouteData = new Microsoft.AspNetCore.Routing.RouteData(),
+ ActionDescriptor = new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor()
+ },
+ FieldName = "model",
+ BindingSource = BindingSource.Custom,
+ HttpContext = httpContext,
+ };
+ }
+}
\ No newline at end of file