diff --git a/Directory.Packages.props b/Directory.Packages.props
index a3bfb51cce81..b43fec60321c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -89,4 +89,4 @@
-
+
\ No newline at end of file
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/BatchSchemasDataTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/BatchSchemasDataTypeController.cs
new file mode 100644
index 000000000000..989137a678e1
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/BatchSchemasDataTypeController.cs
@@ -0,0 +1,71 @@
+using Asp.Versioning;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.ViewModels;
+using Umbraco.Cms.Api.Management.ViewModels.DataType;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+
+namespace Umbraco.Cms.Api.Management.Controllers.DataType;
+
+///
+/// Controller for retrieving multiple data type value schemas in a single request.
+///
+[ApiVersion("1.0")]
+public class BatchSchemasDataTypeController : DataTypeControllerBase
+{
+ private readonly IPropertyEditorSchemaService _schemaService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The property editor schema service.
+ public BatchSchemasDataTypeController(IPropertyEditorSchemaService schemaService)
+ => _schemaService = schemaService;
+
+ ///
+ /// Gets the value schemas for multiple data types.
+ ///
+ /// A cancellation token.
+ /// The unique identifiers of the data types.
+ /// The schema information for the requested data types.
+ ///
+ /// Returns schema information for property editors that implement IValueSchemaProvider.
+ /// Each item includes an error field if the schema could not be retrieved (e.g., data type not found or schema not supported).
+ ///
+ [HttpGet("schemas/batch")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(FetchResponseModel), StatusCodes.Status200OK)]
+ public async Task GetSchemas(
+ CancellationToken cancellationToken,
+ [FromQuery(Name = "id")] Guid[] ids)
+ {
+ Guid[] requestedIds = [.. ids.Distinct()];
+
+ if (requestedIds.Length == 0)
+ {
+ return Ok(new FetchResponseModel());
+ }
+
+ var items = new List();
+
+ foreach (Guid id in requestedIds)
+ {
+ Attempt attempt = await _schemaService.GetSchemaAsync(id);
+ items.Add(new DataTypeSchemaItemResponseModel
+ {
+ Id = id,
+ ValueTypeName = attempt.Success ? attempt.Result.ValueType?.FullName : null,
+ JsonSchema = attempt.Success ? attempt.Result.JsonSchema : null,
+ Error = attempt.Success ? null : attempt.Status.ToString(),
+ });
+ }
+
+ return Ok(new FetchResponseModel
+ {
+ Total = items.Count,
+ Items = items,
+ });
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs
index 84f70e91c49a..9ac268c65413 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
@@ -55,6 +55,21 @@ protected IActionResult DataTypeOperationStatusResult(DataTypeOperationStatus st
protected IActionResult DataTypeNotFound() => OperationStatusResult(DataTypeOperationStatus.NotFound, DataTypeNotFound);
+ protected IActionResult PropertyEditorSchemaOperationStatusResult(PropertyEditorSchemaOperationStatus status) =>
+ OperationStatusResult(status, problemDetailsBuilder => status switch
+ {
+ PropertyEditorSchemaOperationStatus.DataTypeNotFound => NotFound(problemDetailsBuilder
+ .WithTitle("The data type could not be found")
+ .Build()),
+ PropertyEditorSchemaOperationStatus.SchemaNotSupported => NotFound(problemDetailsBuilder
+ .WithTitle("Schema not supported")
+ .WithDetail("The property editor for this data type does not support schema information.")
+ .Build()),
+ _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
+ .WithTitle("Unknown property editor schema operation status.")
+ .Build()),
+ });
+
private IActionResult DataTypeNotFound(ProblemDetailsBuilder problemDetailsBuilder)
=> NotFound(problemDetailsBuilder
.WithTitle("The data type could not be found")
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/SchemaDataTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/SchemaDataTypeController.cs
new file mode 100644
index 000000000000..45f43e08f614
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/SchemaDataTypeController.cs
@@ -0,0 +1,54 @@
+using Asp.Versioning;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.ViewModels.DataType;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+
+namespace Umbraco.Cms.Api.Management.Controllers.DataType;
+
+///
+/// Controller for retrieving data type value schemas.
+///
+[ApiVersion("1.0")]
+public class SchemaDataTypeController : DataTypeControllerBase
+{
+ private readonly IPropertyEditorSchemaService _schemaService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The property editor schema service.
+ public SchemaDataTypeController(IPropertyEditorSchemaService schemaService)
+ => _schemaService = schemaService;
+
+ ///
+ /// Gets the value schema for a data type.
+ ///
+ /// The unique identifier of the data type.
+ /// The schema information for the data type's values.
+ ///
+ /// Returns schema information for property editors that implement IValueSchemaProvider.
+ /// Returns 404 if the data type is not found or doesn't support schema information.
+ ///
+ [HttpGet("{id:guid}/schema")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(DataTypeSchemaResponseModel), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ public async Task Schema(Guid id)
+ {
+ Attempt attempt = await _schemaService.GetSchemaAsync(id);
+ if (attempt.Success is false)
+ {
+ return PropertyEditorSchemaOperationStatusResult(attempt.Status);
+ }
+
+ PropertyValueSchema result = attempt.Result;
+ return Ok(new DataTypeSchemaResponseModel
+ {
+ ValueTypeName = result.ValueType?.FullName,
+ JsonSchema = result.JsonSchema,
+ });
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/SchemaDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/SchemaDocumentTypeController.cs
new file mode 100644
index 000000000000..f83ba7a31d23
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/SchemaDocumentTypeController.cs
@@ -0,0 +1,44 @@
+using System.Text.Json.Nodes;
+using Asp.Versioning;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+
+namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
+
+///
+/// Controller for retrieving document type JSON schemas.
+///
+[ApiVersion("1.0")]
+public class SchemaDocumentTypeController : DocumentTypeControllerBase
+{
+ private readonly IContentTypeJsonSchemaService _schemaService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SchemaDocumentTypeController(IContentTypeJsonSchemaService schemaService)
+ => _schemaService = schemaService;
+
+ ///
+ /// Gets a JSON Schema for a specific document type.
+ ///
+ /// The unique identifier of the document type.
+ /// A JSON Schema describing the document creation/update payload structure.
+ ///
+ /// The returned JSON Schema references data type schemas via external $ref URIs.
+ /// Tooling should resolve these references by making HTTP requests to the data type schema endpoints.
+ ///
+ [HttpGet("{id:guid}/schema")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(JsonObject), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ public async Task GetSchema(Guid id)
+ {
+ JsonObject? schema = await _schemaService.GetDocumentTypeSchemaAsync(id);
+ return schema is not null
+ ? Ok(schema)
+ : OperationStatusResult(ContentTypeOperationStatus.NotFound);
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/SchemaMediaTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/SchemaMediaTypeController.cs
new file mode 100644
index 000000000000..309105891cbb
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/SchemaMediaTypeController.cs
@@ -0,0 +1,44 @@
+using System.Text.Json.Nodes;
+using Asp.Versioning;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+
+namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
+
+///
+/// Controller for retrieving media type JSON schemas.
+///
+[ApiVersion("1.0")]
+public class SchemaMediaTypeController : MediaTypeControllerBase
+{
+ private readonly IContentTypeJsonSchemaService _schemaService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SchemaMediaTypeController(IContentTypeJsonSchemaService schemaService)
+ => _schemaService = schemaService;
+
+ ///
+ /// Gets a JSON Schema for a specific media type.
+ ///
+ /// The unique identifier of the media type.
+ /// A JSON Schema describing the media creation/update payload structure.
+ ///
+ /// The returned JSON Schema references data type schemas via external $ref URIs.
+ /// Tooling should resolve these references by making HTTP requests to the data type schema endpoints.
+ ///
+ [HttpGet("{id:guid}/schema")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(JsonObject), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ public async Task GetSchema(Guid id)
+ {
+ JsonObject? schema = await _schemaService.GetMediaTypeSchemaAsync(id);
+ return schema is not null
+ ? Ok(schema)
+ : OperationStatusResult(ContentTypeOperationStatus.NotFound);
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/SchemaMemberTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/SchemaMemberTypeController.cs
new file mode 100644
index 000000000000..03236fe3b0fb
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/SchemaMemberTypeController.cs
@@ -0,0 +1,44 @@
+using System.Text.Json.Nodes;
+using Asp.Versioning;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Umbraco.Cms.Api.Management.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+
+namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
+
+///
+/// Controller for retrieving member type JSON schemas.
+///
+[ApiVersion("1.0")]
+public class SchemaMemberTypeController : MemberTypeControllerBase
+{
+ private readonly IContentTypeJsonSchemaService _schemaService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SchemaMemberTypeController(IContentTypeJsonSchemaService schemaService)
+ => _schemaService = schemaService;
+
+ ///
+ /// Gets a JSON Schema for a specific member type.
+ ///
+ /// The unique identifier of the member type.
+ /// A JSON Schema describing the member creation/update payload structure.
+ ///
+ /// The returned JSON Schema references data type schemas via external $ref URIs.
+ /// Tooling should resolve these references by making HTTP requests to the data type schema endpoints.
+ ///
+ [HttpGet("{id:guid}/schema")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(JsonObject), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ public async Task GetSchema(Guid id)
+ {
+ JsonObject? schema = await _schemaService.GetMemberTypeSchemaAsync(id);
+ return schema is not null
+ ? Ok(schema)
+ : OperationStatusResult(ContentTypeOperationStatus.NotFound);
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs
index 596676bc1912..899d27ad3474 100644
--- a/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs
+++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.DocumentType;
+using Umbraco.Cms.Api.Management.Services;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
@@ -11,6 +12,7 @@ internal static class DocumentTypeBuilderExtensions
internal static IUmbracoBuilder AddDocumentTypes(this IUmbracoBuilder builder)
{
builder.Services.AddTransient();
+ builder.Services.AddTransient();
builder.WithCollectionBuilder().Add();
builder.WithCollectionBuilder().Add();
diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs
index a99e6de55303..f1d977ef4fd5 100644
--- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -21,6 +21,7 @@ public static IUmbracoBuilder AddUmbracoManagementApi(this IUmbracoBuilder build
IServiceCollection services = builder.Services;
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.Services.AddUnique();
builder.AddUmbracoApiOpenApiUI();
diff --git a/src/Umbraco.Cms.Api.Management/Routing/IManagementApiRouteBuilder.cs b/src/Umbraco.Cms.Api.Management/Routing/IManagementApiRouteBuilder.cs
new file mode 100644
index 000000000000..4b1c26fa8c25
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Routing/IManagementApiRouteBuilder.cs
@@ -0,0 +1,20 @@
+using System.Linq.Expressions;
+
+namespace Umbraco.Cms.Api.Management.Routing;
+
+///
+/// Provides URL generation for Management API controllers from non-controller contexts.
+///
+public interface IManagementApiRouteBuilder
+{
+ ///
+ /// Generates a path to an action on a Management API controller.
+ ///
+ /// The controller type.
+ /// Expression selecting the action name (e.g., c => nameof(c.Schema)).
+ /// Route values (e.g., new { id = guid }).
+ /// The generated path, or null if the route could not be found.
+ string? GetPathByAction(
+ Expression> action,
+ object? routeValues = null);
+}
diff --git a/src/Umbraco.Cms.Api.Management/Routing/ManagementApiRouteBuilder.cs b/src/Umbraco.Cms.Api.Management/Routing/ManagementApiRouteBuilder.cs
new file mode 100644
index 000000000000..5e25e63926f2
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Routing/ManagementApiRouteBuilder.cs
@@ -0,0 +1,52 @@
+using System.Linq.Expressions;
+using Asp.Versioning;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Api.Management.Controllers;
+
+namespace Umbraco.Cms.Api.Management.Routing;
+
+///
+/// Provides URL generation for Management API controllers using LinkGenerator.
+///
+///
+/// This service mirrors the CreatedAtId pattern used in ManagementApiControllerBase
+/// but works in non-controller contexts where IUrlHelper is not available.
+///
+internal sealed class ManagementApiRouteBuilder : IManagementApiRouteBuilder
+{
+ private readonly LinkGenerator _linkGenerator;
+ private readonly ApiVersioningOptions _apiVersioningOptions;
+
+ public ManagementApiRouteBuilder(
+ LinkGenerator linkGenerator,
+ IOptions apiVersioningOptions)
+ {
+ _linkGenerator = linkGenerator;
+ _apiVersioningOptions = apiVersioningOptions.Value;
+ }
+
+ ///
+ public string? GetPathByAction(
+ Expression> action,
+ object? routeValues = null)
+ {
+ if (action.Body is not ConstantExpression constantExpression)
+ {
+ throw new ArgumentException("Expression must be a constant expression.", nameof(action));
+ }
+
+ var controllerName = ManagementApiRegexes.ControllerTypeToNameRegex()
+ .Replace(typeof(TController).Name, string.Empty);
+ var actionName = constantExpression.Value?.ToString()
+ ?? throw new ArgumentException("Expression does not have a value.", nameof(action));
+
+ // Merge provided route values with the required API version
+ var allRouteValues = new RouteValueDictionary(routeValues)
+ {
+ ["version"] = _apiVersioningOptions.DefaultApiVersion.MajorVersion?.ToString(),
+ };
+
+ return _linkGenerator.GetPathByAction(actionName, controllerName, allRouteValues);
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/Services/ContentTypeJsonSchemaService.cs b/src/Umbraco.Cms.Api.Management/Services/ContentTypeJsonSchemaService.cs
new file mode 100644
index 000000000000..fa6798ed8fbd
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Services/ContentTypeJsonSchemaService.cs
@@ -0,0 +1,294 @@
+using System.Text.Json.Nodes;
+using Umbraco.Cms.Api.Management.Controllers.DataType;
+using Umbraco.Cms.Api.Management.Routing;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Api.Management.Services;
+
+///
+/// Provides services for generating JSON Schema for content types.
+///
+internal sealed class ContentTypeJsonSchemaService : IContentTypeJsonSchemaService
+{
+ private const string JsonSchemaVersion = "https://json-schema.org/draft/2020-12/schema";
+
+ private readonly IContentTypeService _contentTypeService;
+ private readonly IMediaTypeService _mediaTypeService;
+ private readonly IMemberTypeService _memberTypeService;
+ private readonly IPropertyEditorSchemaService _propertyEditorSchemaService;
+ private readonly IManagementApiRouteBuilder _routeBuilder;
+ private readonly IDataTypeService _dataTypeService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContentTypeJsonSchemaService(
+ IContentTypeService contentTypeService,
+ IMediaTypeService mediaTypeService,
+ IMemberTypeService memberTypeService,
+ IPropertyEditorSchemaService propertyEditorSchemaService,
+ IManagementApiRouteBuilder routeBuilder,
+ IDataTypeService dataTypeService)
+ {
+ _contentTypeService = contentTypeService;
+ _mediaTypeService = mediaTypeService;
+ _memberTypeService = memberTypeService;
+ _propertyEditorSchemaService = propertyEditorSchemaService;
+ _routeBuilder = routeBuilder;
+ _dataTypeService = dataTypeService;
+ }
+
+ ///
+ public async Task GetDocumentTypeSchemaAsync(Guid key)
+ {
+ IContentType? contentType = await _contentTypeService.GetAsync(key);
+ return contentType is null ? null : await BuildSchemaAsync(contentType, "document");
+ }
+
+ ///
+ public async Task GetMediaTypeSchemaAsync(Guid key)
+ {
+ IMediaType? mediaType = await _mediaTypeService.GetAsync(key);
+ return mediaType is null ? null : await BuildSchemaAsync(mediaType, "media");
+ }
+
+ ///
+ public async Task GetMemberTypeSchemaAsync(Guid key)
+ {
+ IMemberType? memberType = await _memberTypeService.GetAsync(key);
+ return memberType is null ? null : await BuildSchemaAsync(memberType, "member");
+ }
+
+ private async Task BuildSchemaAsync(IContentTypeComposition contentType, string contentKind)
+ {
+ var schema = new JsonObject
+ {
+ ["$schema"] = JsonSchemaVersion,
+ ["$id"] = $"urn:umbraco:{contentKind}-type:{contentType.Key}",
+ ["title"] = $"Create {contentType.Name}",
+ ["description"] = $"JSON Schema for creating/updating {contentKind} content of type '{contentType.Alias}'",
+ ["type"] = "object",
+ ["required"] = new JsonArray($"{contentKind}Type", "values", "variants"),
+ };
+
+ // Build properties
+ var properties = new JsonObject
+ {
+ [$"{contentKind}Type"] = BuildContentTypeReference(contentType.Key),
+ ["id"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "uuid",
+ ["description"] = "Optional key for the new content item",
+ },
+ ["values"] = await BuildValuesSchemaAsync(contentType),
+ ["variants"] = new JsonObject { ["$ref"] = "#/$defs/variants" },
+ };
+
+ // Add parent reference for documents and media (not members)
+ if (contentKind is "document" or "media")
+ {
+ properties["parent"] = new JsonObject { ["$ref"] = "#/$defs/referenceById" };
+ }
+
+ // Add template reference for documents only
+ if (contentKind == "document")
+ {
+ properties["template"] = new JsonObject { ["$ref"] = "#/$defs/referenceById" };
+ }
+
+ schema["properties"] = properties;
+
+ // Build $defs
+ schema["$defs"] = BuildDefs();
+
+ // Build x-umbraco-content-type metadata
+ schema["x-umbraco-content-type"] = new JsonObject
+ {
+ ["key"] = contentType.Key.ToString(),
+ ["alias"] = contentType.Alias,
+ ["isElement"] = contentType.IsElement,
+ ["variations"] = contentType.Variations.ToString(),
+ };
+
+ return schema;
+ }
+
+ private static JsonObject BuildContentTypeReference(Guid contentTypeKey)
+ => new()
+ {
+ ["type"] = "object",
+ ["required"] = new JsonArray("id"),
+ ["properties"] = new JsonObject
+ {
+ ["id"] = new JsonObject
+ {
+ ["const"] = contentTypeKey.ToString(),
+ ["description"] = "Content type identifier",
+ },
+ },
+ };
+
+ private async Task BuildValuesSchemaAsync(IContentTypeComposition contentType)
+ {
+ IPropertyType[] propertyTypes = contentType.CompositionPropertyTypes.ToArray();
+
+ // Build the items schema with if/then clauses for each property
+ var itemsAllOf = new JsonArray
+ {
+ new JsonObject { ["$ref"] = "#/$defs/valueBase" },
+ };
+
+ // Add if/then clause for each property that supports schema
+ foreach (IPropertyType propertyType in propertyTypes)
+ {
+ JsonObject ifThen = BuildPropertyIfThen(propertyType);
+ itemsAllOf.Add(ifThen);
+ }
+
+ // get all relevant datatypes
+ IDataType[] dataTypes = (await _dataTypeService.GetAllAsync(propertyTypes.Select(propertyType => propertyType.DataTypeKey).Distinct()
+ .ToArray())).ToArray();
+
+ // Index by key for O(1) lookup
+ Dictionary dataTypesByKey = dataTypes.ToDictionary(dt => dt.Key);
+
+ // Build x-umbraco-properties metadata
+ var propertiesMetadata = new JsonObject();
+ foreach (IPropertyType propertyType in propertyTypes)
+ {
+ propertiesMetadata[propertyType.Alias] = new JsonObject
+ {
+ ["dataTypeId"] = propertyType.DataTypeKey.ToString(),
+ ["editorAlias"] = propertyType.PropertyEditorAlias,
+ ["editorUiAlias"] = dataTypesByKey.GetValueOrDefault(propertyType.DataTypeKey)?.EditorUiAlias,
+ ["mandatory"] = propertyType.Mandatory,
+ ["variations"] = propertyType.Variations.ToString(),
+ };
+ }
+
+ return new JsonObject
+ {
+ ["type"] = "array",
+ ["description"] = "Property values for the content item",
+ ["items"] = new JsonObject { ["allOf"] = itemsAllOf },
+ ["x-umbraco-properties"] = propertiesMetadata,
+ };
+ }
+
+ private JsonObject BuildPropertyIfThen(IPropertyType propertyType)
+ {
+ var thenProperties = new JsonObject();
+
+ // Check if the property editor supports schema
+ if (_propertyEditorSchemaService.SupportsSchema(propertyType.PropertyEditorAlias))
+ {
+ // Add $ref to the data type schema endpoint
+ var schemaPath = _routeBuilder.GetPathByAction(
+ c => nameof(c.Schema),
+ new { id = propertyType.DataTypeKey });
+
+ thenProperties["value"] = new JsonObject
+ {
+ ["$ref"] = schemaPath,
+ };
+ }
+
+ // If no schema support, value can be anything (already defined in valueBase)
+
+ return new JsonObject
+ {
+ ["if"] = new JsonObject
+ {
+ ["properties"] = new JsonObject
+ {
+ ["alias"] = new JsonObject { ["const"] = propertyType.Alias },
+ },
+ },
+ ["then"] = new JsonObject { ["properties"] = thenProperties },
+ };
+ }
+
+ private static JsonObject BuildDefs()
+ => new()
+ {
+ ["referenceById"] = new JsonObject
+ {
+ ["oneOf"] = new JsonArray
+ {
+ new JsonObject { ["type"] = "null" },
+ new JsonObject
+ {
+ ["type"] = "object",
+ ["required"] = new JsonArray("id"),
+ ["properties"] = new JsonObject
+ {
+ ["id"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["format"] = "uuid",
+ },
+ },
+ },
+ },
+ },
+ ["valueBase"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["required"] = new JsonArray("alias"),
+ ["properties"] = new JsonObject
+ {
+ ["alias"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["description"] = "Property type alias",
+ },
+ ["culture"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Culture code for variant properties, or null for invariant",
+ },
+ ["segment"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Segment identifier, or null for non-segmented",
+ },
+ ["value"] = new JsonObject
+ {
+ ["description"] = "Property value (type depends on property editor)",
+ },
+ },
+ },
+ ["variants"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["minItems"] = 1,
+ ["description"] = "Content variants (at minimum one variant with the name is required)",
+ ["items"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["required"] = new JsonArray("name"),
+ ["properties"] = new JsonObject
+ {
+ ["name"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["minLength"] = 1,
+ ["description"] = "Content name for this variant",
+ },
+ ["culture"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Culture code for this variant, or null for invariant",
+ },
+ ["segment"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Segment identifier, or null for non-segmented",
+ },
+ },
+ },
+ },
+ };
+}
diff --git a/src/Umbraco.Cms.Api.Management/Services/IContentTypeJsonSchemaService.cs b/src/Umbraco.Cms.Api.Management/Services/IContentTypeJsonSchemaService.cs
new file mode 100644
index 000000000000..d8c3d599521e
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Services/IContentTypeJsonSchemaService.cs
@@ -0,0 +1,47 @@
+using System.Text.Json.Nodes;
+
+namespace Umbraco.Cms.Api.Management.Services;
+
+///
+/// Provides services for generating JSON Schema for content types.
+///
+///
+///
+/// This service generates JSON Schema (draft 2020-12) that describes the structure of content
+/// creation payloads for specific content types (documents, media, members).
+///
+///
+/// The generated schemas reference data type schemas via external $ref URIs to the
+/// /umbraco/management/api/v1/data-type/{id}/schema endpoint. Tooling should resolve
+/// these references by making HTTP requests to the referenced endpoints.
+///
+///
+public interface IContentTypeJsonSchemaService
+{
+ ///
+ /// Gets a JSON Schema for creating/updating documents of a specific document type.
+ ///
+ /// The unique key of the document type.
+ ///
+ /// A JSON Schema as a , or null if the document type was not found.
+ ///
+ Task GetDocumentTypeSchemaAsync(Guid key);
+
+ ///
+ /// Gets a JSON Schema for creating/updating media of a specific media type.
+ ///
+ /// The unique key of the media type.
+ ///
+ /// A JSON Schema as a , or null if the media type was not found.
+ ///
+ Task GetMediaTypeSchemaAsync(Guid key);
+
+ ///
+ /// Gets a JSON Schema for creating/updating members of a specific member type.
+ ///
+ /// The unique key of the member type.
+ ///
+ /// A JSON Schema as a , or null if the member type was not found.
+ ///
+ Task GetMemberTypeSchemaAsync(Guid key);
+}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeSchemaItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeSchemaItemResponseModel.cs
new file mode 100644
index 000000000000..1a72bff81a16
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeSchemaItemResponseModel.cs
@@ -0,0 +1,30 @@
+using System.Text.Json.Nodes;
+
+namespace Umbraco.Cms.Api.Management.ViewModels.DataType;
+
+///
+/// Represents the schema information for a single data type in a batch response.
+///
+public class DataTypeSchemaItemResponseModel
+{
+ ///
+ /// Gets or sets the unique identifier of the data type.
+ ///
+ public required Guid Id { get; set; }
+
+ ///
+ /// Gets or sets the full name of the CLR type for property values, if available.
+ ///
+ public string? ValueTypeName { get; set; }
+
+ ///
+ /// Gets or sets the JSON Schema for property values, if available.
+ ///
+ public JsonObject? JsonSchema { get; set; }
+
+ ///
+ /// Gets or sets the error status if the schema could not be retrieved.
+ /// Possible values: "DataTypeNotFound", "SchemaNotSupported", or null if successful.
+ ///
+ public string? Error { get; set; }
+}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeSchemaResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeSchemaResponseModel.cs
new file mode 100644
index 000000000000..956f349283d0
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeSchemaResponseModel.cs
@@ -0,0 +1,26 @@
+using System.Text.Json.Nodes;
+
+namespace Umbraco.Cms.Api.Management.ViewModels.DataType;
+
+///
+/// Represents schema information for a data type's stored values.
+///
+public class DataTypeSchemaResponseModel
+{
+ ///
+ /// Gets or sets the full name of the CLR type representing stored values.
+ ///
+ ///
+ /// This can be null if the property editor doesn't provide type information
+ /// or if the type varies significantly based on configuration.
+ ///
+ public string? ValueTypeName { get; set; }
+
+ ///
+ /// Gets or sets the JSON Schema (draft 2020-12) describing the value structure.
+ ///
+ ///
+ /// This can be null if the property editor doesn't provide schema information.
+ ///
+ public JsonObject? JsonSchema { get; set; }
+}
diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/FetchResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/FetchResponseModel.cs
new file mode 100644
index 000000000000..27e2d7ca5c9b
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/ViewModels/FetchResponseModel.cs
@@ -0,0 +1,18 @@
+namespace Umbraco.Cms.Api.Management.ViewModels;
+
+///
+/// Represents a response model for fetching multiple entities, including the total count and the collection of items.
+///
+/// The entity response model type.
+public class FetchResponseModel
+{
+ ///
+ /// Gets or sets the total number of entities returned.
+ ///
+ public int Total { get; set; }
+
+ ///
+ /// Gets or sets the collection of fetched entities.
+ ///
+ public IEnumerable Items { get; set; } = [];
+}
diff --git a/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs
index 747ab67d82d5..0c98f5643e62 100644
--- a/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
@@ -20,7 +21,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.ContentPicker,
ValueType = ValueTypes.String,
ValueEditorIsReusable = true)]
-public class ContentPickerPropertyEditor : DataEditor
+public class ContentPickerPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
@@ -36,6 +37,19 @@ public ContentPickerPropertyEditor(IDataValueEditorFactory dataValueEditorFactor
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ ["description"] = "GUID of the selected document",
+ };
+
///
protected override IConfigurationEditor CreateConfigurationEditor() =>
new ContentPickerConfigurationEditor(_ioHelper);
diff --git a/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs
index 9be861bff6f7..378264387023 100644
--- a/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -20,7 +21,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.Decimal,
ValueType = ValueTypes.Decimal,
ValueEditorIsReusable = true)]
-public class DecimalPropertyEditor : DataEditor
+public class DecimalPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -30,6 +31,40 @@ public DecimalPropertyEditor(
: base(dataValueEditorFactory) =>
SupportsReadOnly = true;
+ ///
+ public Type? GetValueType(object? configuration) => typeof(decimal?);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration)
+ {
+ var schema = new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("number", "null"),
+ };
+
+ // Add min/max/step constraints from configuration if available
+ if (configuration is IDictionary configDict)
+ {
+ if (configDict.TryGetValue("min", out var minValue) && minValue is double min)
+ {
+ schema["minimum"] = min;
+ }
+
+ if (configDict.TryGetValue("max", out var maxValue) && maxValue is double max)
+ {
+ schema["maximum"] = max;
+ }
+
+ if (configDict.TryGetValue("step", out var stepValue) && stepValue is double step && step > 0)
+ {
+ schema["multipleOf"] = step;
+ }
+ }
+
+ return schema;
+ }
+
///
protected override IDataValueEditor CreateValueEditor()
=> DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs
index 39633c1a509d..98168a6a0cc0 100644
--- a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerPropertyEditor.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Nodes;
+
namespace Umbraco.Cms.Core.PropertyEditors;
///
@@ -6,7 +8,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataEditor(
Constants.PropertyEditors.Aliases.ColorPickerEyeDropper,
ValueEditorIsReusable = true)]
-public class EyeDropperColorPickerPropertyEditor : DataEditor
+public class EyeDropperColorPickerPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -15,4 +17,16 @@ public class EyeDropperColorPickerPropertyEditor : DataEditor
public EyeDropperColorPickerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["pattern"] = "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$",
+ ["description"] = "Hex color value (e.g., #FF0000 or #FF0000FF with alpha)",
+ };
}
diff --git a/src/Umbraco.Core/PropertyEditors/IValueSchemaProvider.cs b/src/Umbraco.Core/PropertyEditors/IValueSchemaProvider.cs
new file mode 100644
index 000000000000..7f4569e2c591
--- /dev/null
+++ b/src/Umbraco.Core/PropertyEditors/IValueSchemaProvider.cs
@@ -0,0 +1,58 @@
+using System.Text.Json.Nodes;
+
+namespace Umbraco.Cms.Core.PropertyEditors;
+
+///
+/// Provides schema information about the values a property editor accepts.
+///
+///
+///
+/// This interface is opt-in for property editors that want to expose their value schema
+/// for programmatic content creation, validation, and tooling support.
+///
+///
+/// Implementations should return the schema for the incoming value (what
+/// receives via ), not the stored/database model (what
+/// produces) or the published model (what
+/// produces).
+///
+///
+public interface IValueSchemaProvider
+{
+ ///
+ /// Gets the CLR type that represents the incoming value structure.
+ ///
+ /// The data type configuration, which may affect the value type.
+ ///
+ /// The CLR type of the incoming value, or null if the type cannot be determined
+ /// or varies significantly based on configuration.
+ ///
+ ///
+ ///
+ /// For simple editors (e.g., textbox), this might return .
+ /// For complex editors (e.g., MediaPicker3), this returns the DTO type that the editor submits.
+ /// For block-based editors where the structure is entirely configuration-dependent, this may return null.
+ ///
+ ///
+ Type? GetValueType(object? configuration);
+
+ ///
+ /// Gets a JSON Schema (draft 2020-12) describing the incoming value structure.
+ ///
+ /// The data type configuration, which may affect the schema.
+ ///
+ /// A containing a valid JSON Schema, or null if the schema
+ /// cannot be generated for the given configuration.
+ ///
+ ///
+ ///
+ /// The returned schema should describe the structure that receives
+ /// (i.e., what the Management API accepts).
+ ///
+ ///
+ /// For configuration-dependent schemas (e.g., BlockList with specific element types), the schema
+ /// should reflect the constraints defined in the configuration.
+ ///
+ ///
+ JsonObject? GetValueSchema(object? configuration);
+}
diff --git a/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs
index fd0ff9315833..b72cc701e57f 100644
--- a/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -20,7 +21,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.Integer,
ValueType = ValueTypes.Integer,
ValueEditorIsReusable = true)]
-public class IntegerPropertyEditor : DataEditor
+public class IntegerPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -29,6 +30,40 @@ public IntegerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+ ///
+ public Type? GetValueType(object? configuration) => typeof(int?);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration)
+ {
+ var schema = new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("integer", "null"),
+ };
+
+ // Add min/max constraints from configuration if available
+ if (configuration is IDictionary configDict)
+ {
+ if (configDict.TryGetValue("min", out var minValue) && minValue is int min)
+ {
+ schema["minimum"] = min;
+ }
+
+ if (configDict.TryGetValue("max", out var maxValue) && maxValue is int max)
+ {
+ schema["maximum"] = max;
+ }
+
+ if (configDict.TryGetValue("step", out var stepValue) && stepValue is int step && step > 1)
+ {
+ schema["multipleOf"] = step;
+ }
+ }
+
+ return schema;
+ }
+
///
protected override IDataValueEditor CreateValueEditor()
=> DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs
index dbc74a718da4..52ee0112a173 100644
--- a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
@@ -14,7 +15,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataEditor(
Constants.PropertyEditors.Aliases.Label,
ValueEditorIsReusable = true)]
-public class LabelPropertyEditor : DataEditor
+public class LabelPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
@@ -28,6 +29,17 @@ public LabelPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIOHe
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Read-only value, any value provided will be ignored",
+ };
+
///
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs
index 39693f3c3470..51a78bb6d31a 100644
--- a/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/MarkdownPropertyEditor.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -12,7 +13,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.MarkdownEditor,
ValueType = ValueTypes.Text,
ValueEditorIsReusable = true)]
-public class MarkdownPropertyEditor : DataEditor
+public class MarkdownPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -21,6 +22,17 @@ public MarkdownPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Markdown formatted text",
+ };
+
///
/// Create a custom value editor
///
diff --git a/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs
index ff26712a4597..c83a3c530625 100644
--- a/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/MemberGroupPickerPropertyEditor.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -15,7 +16,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.MemberGroupPicker,
ValueType = ValueTypes.Text,
ValueEditorIsReusable = true)]
-public class MemberGroupPickerPropertyEditor : DataEditor
+public class MemberGroupPickerPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -25,6 +26,17 @@ public MemberGroupPickerPropertyEditor(IDataValueEditorFactory dataValueEditorFa
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Comma-separated list of member group GUIDs",
+ };
+
///
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs
index 7dd3ee2ada01..0a4b38d2e443 100644
--- a/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/MemberPickerPropertyEditor.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -15,7 +16,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.MemberPicker,
ValueType = ValueTypes.String,
ValueEditorIsReusable = true)]
-public class MemberPickerPropertyEditor : DataEditor
+public class MemberPickerPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -25,6 +26,19 @@ public MemberPickerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+ ///
+ public Type? GetValueType(object? configuration) => typeof(Guid?);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ ["description"] = "GUID of the selected member",
+ };
+
///
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Core/PropertyEditors/PlainDateTimePropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PlainDateTimePropertyEditor.cs
index 737f5b4db20e..fea6f8fffcd7 100644
--- a/src/Umbraco.Core/PropertyEditors/PlainDateTimePropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/PlainDateTimePropertyEditor.cs
@@ -1,6 +1,8 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
+
namespace Umbraco.Cms.Core.PropertyEditors;
///
@@ -10,7 +12,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.PlainDateTime,
ValueEditorIsReusable = true,
ValueType = ValueTypes.DateTime)]
-public class PlainDateTimePropertyEditor : DataEditor
+public class PlainDateTimePropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -18,4 +20,16 @@ public class PlainDateTimePropertyEditor : DataEditor
public PlainDateTimePropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+
+ ///
+ public Type? GetValueType(object? configuration) => typeof(DateTime?);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "date-time",
+ ["description"] = "ISO 8601 date-time string",
+ };
}
diff --git a/src/Umbraco.Core/PropertyEditors/PlainDecimalPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PlainDecimalPropertyEditor.cs
index 89b7442662c4..b48652de9741 100644
--- a/src/Umbraco.Core/PropertyEditors/PlainDecimalPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/PlainDecimalPropertyEditor.cs
@@ -1,4 +1,6 @@
-namespace Umbraco.Cms.Core.PropertyEditors;
+using System.Text.Json.Nodes;
+
+namespace Umbraco.Cms.Core.PropertyEditors;
///
/// Represents a property editor for configuration-less decimal properties.
@@ -7,7 +9,7 @@
Constants.PropertyEditors.Aliases.PlainDecimal,
ValueEditorIsReusable = true,
ValueType = ValueTypes.Decimal)]
-public class PlainDecimalPropertyEditor : DataEditor
+public class PlainDecimalPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -15,4 +17,15 @@ public class PlainDecimalPropertyEditor : DataEditor
public PlainDecimalPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+
+ ///
+ public Type? GetValueType(object? configuration) => typeof(decimal?);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("number", "null"),
+ ["description"] = "Plain decimal number value",
+ };
}
diff --git a/src/Umbraco.Core/PropertyEditors/PlainIntegerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PlainIntegerPropertyEditor.cs
index 3cedbd19fb5e..7e98c1b941e3 100644
--- a/src/Umbraco.Core/PropertyEditors/PlainIntegerPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/PlainIntegerPropertyEditor.cs
@@ -1,13 +1,15 @@
-namespace Umbraco.Cms.Core.PropertyEditors;
+using System.Text.Json.Nodes;
+
+namespace Umbraco.Cms.Core.PropertyEditors;
///
-/// Represents a property editor for configuration-less decimal properties.
+/// Represents a property editor for configuration-less integer properties.
///
[DataEditor(
Constants.PropertyEditors.Aliases.PlainInteger,
ValueEditorIsReusable = true,
ValueType = ValueTypes.Integer)]
-public class PlainIntegerPropertyEditor : DataEditor
+public class PlainIntegerPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -15,4 +17,15 @@ public class PlainIntegerPropertyEditor : DataEditor
public PlainIntegerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+
+ ///
+ public Type? GetValueType(object? configuration) => typeof(int?);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("integer", "null"),
+ ["description"] = "Plain integer value",
+ };
}
diff --git a/src/Umbraco.Core/PropertyEditors/PlainJsonPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PlainJsonPropertyEditor.cs
index 040dd74023c2..08f5b648e008 100644
--- a/src/Umbraco.Core/PropertyEditors/PlainJsonPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/PlainJsonPropertyEditor.cs
@@ -1,4 +1,6 @@
-namespace Umbraco.Cms.Core.PropertyEditors;
+using System.Text.Json.Nodes;
+
+namespace Umbraco.Cms.Core.PropertyEditors;
///
/// Represents a property editor for configuration-less JSON properties.
@@ -7,7 +9,7 @@
Constants.PropertyEditors.Aliases.PlainJson,
ValueEditorIsReusable = true,
ValueType = ValueTypes.Json)]
-public class PlainJsonPropertyEditor : DataEditor
+public class PlainJsonPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -15,4 +17,14 @@ public class PlainJsonPropertyEditor : DataEditor
public PlainJsonPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+
+ ///
+ public Type? GetValueType(object? configuration) => typeof(object);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["description"] = "Any valid JSON value",
+ };
}
diff --git a/src/Umbraco.Core/PropertyEditors/PlainStringPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PlainStringPropertyEditor.cs
index 16986b1931ad..ba3962d4ca3f 100644
--- a/src/Umbraco.Core/PropertyEditors/PlainStringPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/PlainStringPropertyEditor.cs
@@ -1,4 +1,6 @@
-namespace Umbraco.Cms.Core.PropertyEditors;
+using System.Text.Json.Nodes;
+
+namespace Umbraco.Cms.Core.PropertyEditors;
///
/// Represents a property editor for configuration-less string properties.
@@ -7,7 +9,7 @@
Constants.PropertyEditors.Aliases.PlainString,
ValueEditorIsReusable = true,
ValueType = ValueTypes.Text)] // NOTE: for ease of use it's called "String", but it's really stored as TEXT
-public class PlainStringPropertyEditor : DataEditor
+public class PlainStringPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -15,4 +17,15 @@ public class PlainStringPropertyEditor : DataEditor
public PlainStringPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Plain text string value",
+ };
}
diff --git a/src/Umbraco.Core/PropertyEditors/PlainTimePropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PlainTimePropertyEditor.cs
index 4af7c1d6432d..30fb90b7af7b 100644
--- a/src/Umbraco.Core/PropertyEditors/PlainTimePropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/PlainTimePropertyEditor.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Nodes;
+
namespace Umbraco.Cms.Core.PropertyEditors;
///
@@ -7,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.PlainTime,
ValueEditorIsReusable = true,
ValueType = ValueTypes.Time)]
-public class PlainTimePropertyEditor : DataEditor
+public class PlainTimePropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -16,4 +18,16 @@ public class PlainTimePropertyEditor : DataEditor
public PlainTimePropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+
+ ///
+ public Type? GetValueType(object? configuration) => typeof(TimeOnly?);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "time",
+ ["description"] = "ISO 8601 time string",
+ };
}
diff --git a/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs
index 9e3c27e766b4..4a6fd84fd3a2 100644
--- a/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/UserPickerPropertyEditor.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -15,7 +16,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.UserPicker,
ValueType = ValueTypes.Integer,
ValueEditorIsReusable = true)]
-public class UserPickerPropertyEditor : DataEditor
+public class UserPickerPropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -25,6 +26,19 @@ public UserPickerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+ ///
+ public Type? GetValueType(object? configuration) => typeof(Guid?);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ ["description"] = "GUID of the selected user",
+ };
+
///
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Core/PropertyEditors/ValueSchemaPatterns.cs b/src/Umbraco.Core/PropertyEditors/ValueSchemaPatterns.cs
new file mode 100644
index 000000000000..0bcd2bc9c1a1
--- /dev/null
+++ b/src/Umbraco.Core/PropertyEditors/ValueSchemaPatterns.cs
@@ -0,0 +1,16 @@
+namespace Umbraco.Cms.Core.PropertyEditors;
+
+///
+/// Contains regex patterns used in JSON Schema validation for property editor values.
+///
+public static class ValueSchemaPatterns
+{
+ ///
+ /// Regex pattern for validating UUID/GUID strings.
+ ///
+ ///
+ /// Matches UUIDs with or without hyphens, case-insensitive (lowercase in pattern).
+ /// Examples: "550e8400-e29b-41d4-a716-446655440000" or "550e8400e29b41d4a716446655440000"
+ ///
+ public const string Uuid = "^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$";
+}
diff --git a/src/Umbraco.Core/Services/IPropertyEditorSchemaService.cs b/src/Umbraco.Core/Services/IPropertyEditorSchemaService.cs
new file mode 100644
index 000000000000..9f723044fabc
--- /dev/null
+++ b/src/Umbraco.Core/Services/IPropertyEditorSchemaService.cs
@@ -0,0 +1,57 @@
+using System.Text.Json.Nodes;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.Services.OperationStatus;
+
+namespace Umbraco.Cms.Core.Services;
+
+///
+/// Provides services for querying property editor value schemas.
+///
+///
+///
+/// This service enables retrieval of JSON Schema information for property editor values,
+/// supporting programmatic content creation, import validation, and tooling.
+///
+///
+/// Schema information is only available for property editors that implement .
+///
+///
+public interface IPropertyEditorSchemaService : IService
+{
+ ///
+ /// Gets the complete schema information for a specific data type, including both CLR type and JSON Schema.
+ ///
+ /// The unique key of the data type.
+ ///
+ /// An attempt containing the schema information with both CLR type and JSON Schema,
+ /// or an appropriate operation status if the data type was not found or doesn't support schemas.
+ ///
+ Task> GetSchemaAsync(Guid dataTypeKey);
+
+ ///
+ /// Gets the CLR type for a property editor with the specified configuration.
+ ///
+ /// The alias of the property editor.
+ /// The configuration object for the data type.
+ ///
+ /// The CLR type, or null if the property editor doesn't implement .
+ ///
+ Type? GetValueType(string propertyEditorAlias, object? configuration);
+
+ ///
+ /// Gets the JSON Schema for a property editor with the specified configuration.
+ ///
+ /// The alias of the property editor.
+ /// The configuration object for the data type.
+ ///
+ /// A JSON Schema (draft 2020-12), or null if the property editor doesn't implement .
+ ///
+ JsonObject? GetValueSchema(string propertyEditorAlias, object? configuration);
+
+ ///
+ /// Checks whether a property editor supports schema information.
+ ///
+ /// The alias of the property editor.
+ /// true if the editor implements ; otherwise, false.
+ bool SupportsSchema(string propertyEditorAlias);
+}
diff --git a/src/Umbraco.Core/Services/OperationStatus/PropertyEditorSchemaOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/PropertyEditorSchemaOperationStatus.cs
new file mode 100644
index 000000000000..405e344279d6
--- /dev/null
+++ b/src/Umbraco.Core/Services/OperationStatus/PropertyEditorSchemaOperationStatus.cs
@@ -0,0 +1,22 @@
+namespace Umbraco.Cms.Core.Services.OperationStatus;
+
+///
+/// Represents the status of a property editor schema operation.
+///
+public enum PropertyEditorSchemaOperationStatus
+{
+ ///
+ /// The operation completed successfully.
+ ///
+ Success,
+
+ ///
+ /// The specified data type was not found.
+ ///
+ DataTypeNotFound,
+
+ ///
+ /// The property editor does not support schema information (does not implement IValueSchemaProvider).
+ ///
+ SchemaNotSupported,
+}
diff --git a/src/Umbraco.Core/Services/PropertyValueSchema.cs b/src/Umbraco.Core/Services/PropertyValueSchema.cs
new file mode 100644
index 000000000000..dc007da88e8a
--- /dev/null
+++ b/src/Umbraco.Core/Services/PropertyValueSchema.cs
@@ -0,0 +1,30 @@
+using System.Text.Json.Nodes;
+
+namespace Umbraco.Cms.Core.Services;
+
+///
+/// Represents the schema information for a property editor value.
+///
+public sealed class PropertyValueSchema
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The CLR type of stored values.
+ /// The JSON Schema describing the value structure.
+ public PropertyValueSchema(Type? valueType, JsonObject? jsonSchema)
+ {
+ ValueType = valueType;
+ JsonSchema = jsonSchema;
+ }
+
+ ///
+ /// Gets the CLR type of stored values, or null if the type cannot be determined.
+ ///
+ public Type? ValueType { get; }
+
+ ///
+ /// Gets the JSON Schema (draft 2020-12) describing the value structure, or null if not provided.
+ ///
+ public JsonObject? JsonSchema { get; }
+}
diff --git a/src/Umbraco.Core/Services/SchemaValidationResult.cs b/src/Umbraco.Core/Services/SchemaValidationResult.cs
new file mode 100644
index 000000000000..5ea5f52c3bac
--- /dev/null
+++ b/src/Umbraco.Core/Services/SchemaValidationResult.cs
@@ -0,0 +1,35 @@
+namespace Umbraco.Cms.Core.Services;
+
+///
+/// Represents a validation error from JSON Schema validation.
+///
+public sealed class SchemaValidationResult
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The validation error message.
+ /// The JSON path to the invalid value.
+ /// The JSON Schema keyword that failed validation.
+ public SchemaValidationResult(string message, string? path = null, string? keyword = null)
+ {
+ Message = message;
+ Path = path;
+ Keyword = keyword;
+ }
+
+ ///
+ /// Gets the validation error message.
+ ///
+ public string Message { get; }
+
+ ///
+ /// Gets the JSON path to the invalid value (e.g., "$.items[0].name").
+ ///
+ public string? Path { get; }
+
+ ///
+ /// Gets the JSON Schema keyword that failed validation (e.g., "type", "required", "minLength").
+ ///
+ public string? Keyword { get; }
+}
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
index 2b569ddd5ab6..5fa0248f95c4 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
@@ -87,6 +87,7 @@ internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder)
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
+ builder.Services.AddUnique();
#pragma warning disable CS0618 // Type or member is obsolete
// TODO (V18): Replace this with MarkdigMarkdownToHtmlConverter as the default implementation.
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
index 19e518cdcbf8..5da4d046d6c9 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PropertyEditors;
@@ -13,6 +14,7 @@
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Infrastructure.PropertyEditors;
using Umbraco.Extensions;
using BlockGridAreaConfiguration = Umbraco.Cms.Core.PropertyEditors.BlockGridConfiguration.BlockGridAreaConfiguration;
@@ -21,7 +23,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
///
/// Abstract base class for block grid based editors.
///
-public abstract class BlockGridPropertyEditorBase : DataEditor
+public abstract class BlockGridPropertyEditorBase : DataEditor, IValueSchemaProvider
{
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
@@ -34,6 +36,87 @@ protected BlockGridPropertyEditorBase(IDataValueEditorFactory dataValueEditorFac
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _blockValuePropertyIndexValueFactory;
+ ///
+ public virtual Type? GetValueType(object? configuration) => typeof(string); // JSON string representation
+
+ ///
+ public virtual JsonObject? GetValueSchema(object? configuration)
+ {
+ var config = configuration as BlockGridConfiguration;
+
+ // Build area item schema (BlockGrid-specific)
+ var areaItemSchema = new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["key"] = new JsonObject { ["type"] = "string", ["format"] = "uuid", ["pattern"] = ValueSchemaPatterns.Uuid },
+ ["items"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = new JsonObject { ["$ref"] = "#/$defs/layoutItem" },
+ },
+ },
+ };
+
+ // Build layout item schema (BlockGrid-specific - with columnSpan, rowSpan, areas)
+ JsonObject layoutItemSchema = BlockJsonSchemaHelper.CreateBaseLayoutItemSchema();
+ layoutItemSchema["properties"]!.AsObject()["columnSpan"] = new JsonObject { ["type"] = "integer", ["minimum"] = 1 };
+ layoutItemSchema["properties"]!.AsObject()["rowSpan"] = new JsonObject { ["type"] = "integer", ["minimum"] = 1 };
+ layoutItemSchema["properties"]!.AsObject()["areas"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = areaItemSchema,
+ };
+
+ // Build content and settings data schemas with constraints
+ JsonObject contentDataItemSchema = BlockJsonSchemaHelper.CreateContentDataSchema(config?.Blocks);
+ JsonObject settingsDataItemSchema = BlockJsonSchemaHelper.CreateSettingsDataSchema(config?.Blocks);
+
+ // Build the main schema
+ JsonObject schema = BlockJsonSchemaHelper.CreateRootSchema();
+ schema["$defs"] = new JsonObject
+ {
+ ["layoutItem"] = layoutItemSchema,
+ };
+ schema["properties"] = new JsonObject
+ {
+ ["layout"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ [Constants.PropertyEditors.Aliases.BlockGrid] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = new JsonObject { ["$ref"] = "#/$defs/layoutItem" },
+ },
+ },
+ },
+ ["contentData"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = contentDataItemSchema,
+ },
+ ["settingsData"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = settingsDataItemSchema,
+ },
+ };
+
+ // Add grid columns constraint from configuration (BlockGrid-specific)
+ if (config?.GridColumns is int gridColumns && gridColumns > 0)
+ {
+ layoutItemSchema["properties"]!["columnSpan"]!.AsObject()["maximum"] = gridColumns;
+ }
+
+ // Add validation constraints
+ var layoutArray = schema["properties"]!["layout"]!["properties"]![Constants.PropertyEditors.Aliases.BlockGrid]!.AsObject();
+ BlockJsonSchemaHelper.ApplyValidationConstraints(layoutArray, config?.ValidationLimit?.Min, config?.ValidationLimit?.Max);
+
+ return schema;
+ }
#region Value Editor
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockJsonSchemaHelper.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockJsonSchemaHelper.cs
new file mode 100644
index 000000000000..f2c38c328d19
--- /dev/null
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockJsonSchemaHelper.cs
@@ -0,0 +1,188 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.Text.Json.Nodes;
+using Umbraco.Cms.Core.PropertyEditors;
+
+namespace Umbraco.Cms.Infrastructure.PropertyEditors;
+
+///
+/// Helper class for building JSON schemas for block-based property editors.
+///
+internal static class BlockJsonSchemaHelper
+{
+ ///
+ /// Creates the base block item data schema used by all block editors.
+ /// Contains key, contentTypeKey, and values array structure.
+ ///
+ /// A JsonObject representing the block item data schema.
+ public static JsonObject CreateBlockItemDataSchema() =>
+ new()
+ {
+ ["type"] = "object",
+ ["required"] = new JsonArray("key", "contentTypeKey"),
+ ["properties"] = new JsonObject
+ {
+ ["key"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ },
+ ["contentTypeKey"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ },
+ ["values"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["alias"] = new JsonObject { ["type"] = "string" },
+ ["culture"] = new JsonObject { ["type"] = new JsonArray("string", "null") },
+ ["segment"] = new JsonObject { ["type"] = new JsonArray("string", "null") },
+ ["value"] = new JsonObject { }, // Any type - depends on property editor
+ },
+ },
+ },
+ },
+ };
+
+ ///
+ /// Creates a content data schema with allowed content type constraints from block configuration.
+ ///
+ /// The block configurations containing content element type keys.
+ /// A JsonObject representing the content data item schema with type constraints.
+ public static JsonObject CreateContentDataSchema(ICollection? blocks)
+ {
+ JsonObject schema = CreateBlockItemDataSchema();
+
+ if (blocks is null || blocks.Count == 0)
+ {
+ return schema;
+ }
+
+ var allowedContentTypes = new JsonArray();
+ foreach (IBlockConfiguration block in blocks)
+ {
+ allowedContentTypes.Add(JsonValue.Create(block.ContentElementTypeKey.ToString()));
+ }
+
+ schema["properties"]!["contentTypeKey"]!.AsObject()["enum"] = allowedContentTypes;
+ return schema;
+ }
+
+ ///
+ /// Creates a settings data schema with allowed settings type constraints from block configuration.
+ ///
+ /// The block configurations containing settings element type keys.
+ /// A JsonObject representing the settings data item schema with type constraints.
+ public static JsonObject CreateSettingsDataSchema(ICollection? blocks)
+ {
+ JsonObject schema = CreateBlockItemDataSchema();
+
+ if (blocks is null || blocks.Count == 0)
+ {
+ return schema;
+ }
+
+ var allowedSettingsTypes = new JsonArray();
+ foreach (IBlockConfiguration block in blocks)
+ {
+ if (block.SettingsElementTypeKey.HasValue)
+ {
+ allowedSettingsTypes.Add(JsonValue.Create(block.SettingsElementTypeKey.Value.ToString()));
+ }
+ }
+
+ if (allowedSettingsTypes.Count > 0)
+ {
+ schema["properties"]!["contentTypeKey"]!.AsObject()["enum"] = allowedSettingsTypes;
+ }
+
+ return schema;
+ }
+
+ ///
+ /// Creates the base layout item schema with contentKey and settingsKey.
+ /// Used as the foundation for all block layout items.
+ ///
+ /// A JsonObject representing the base layout item schema.
+ public static JsonObject CreateBaseLayoutItemSchema() =>
+ new()
+ {
+ ["type"] = "object",
+ ["required"] = new JsonArray("contentKey"),
+ ["properties"] = new JsonObject
+ {
+ ["contentKey"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ },
+ ["settingsKey"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ },
+ },
+ };
+
+ ///
+ /// Creates the expose item schema for block variation.
+ ///
+ /// A JsonObject representing the expose item schema.
+ public static JsonObject CreateExposeItemSchema() =>
+ new()
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["contentKey"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ },
+ ["culture"] = new JsonObject { ["type"] = new JsonArray("string", "null") },
+ ["segment"] = new JsonObject { ["type"] = new JsonArray("string", "null") },
+ },
+ };
+
+ ///
+ /// Applies minItems/maxItems validation constraints to a layout array schema.
+ ///
+ /// The layout array schema to modify.
+ /// Optional minimum number of items.
+ /// Optional maximum number of items.
+ public static void ApplyValidationConstraints(JsonObject layoutArraySchema, int? min, int? max)
+ {
+ if (min is int minValue && minValue > 0)
+ {
+ layoutArraySchema["minItems"] = minValue;
+ }
+
+ if (max is int maxValue && maxValue > 0)
+ {
+ layoutArraySchema["maxItems"] = maxValue;
+ }
+ }
+
+ ///
+ /// Creates the root schema wrapper with $schema and nullable object type.
+ ///
+ /// A JsonObject with the base schema structure.
+ public static JsonObject CreateRootSchema() =>
+ new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("object", "null"),
+ };
+}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
index 322d2927fb7a..51bc6ba6f2a1 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PropertyEditors;
@@ -10,6 +11,7 @@
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Infrastructure.PropertyEditors;
using Umbraco.Cms.Infrastructure.PropertyEditors.Validators;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -17,7 +19,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
///
/// Abstract base class for block list based editors.
///
-public abstract class BlockListPropertyEditorBase : DataEditor
+public abstract class BlockListPropertyEditorBase : DataEditor, IValueSchemaProvider
{
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
private readonly IJsonSerializer _jsonSerializer;
@@ -36,6 +38,64 @@ protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFac
///
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _blockValuePropertyIndexValueFactory;
+ ///
+ public virtual Type? GetValueType(object? configuration) => typeof(string); // JSON string representation
+
+ ///
+ public virtual JsonObject? GetValueSchema(object? configuration)
+ {
+ var config = configuration as BlockListConfiguration;
+
+ // Build layout item schema
+ JsonObject layoutItemSchema = BlockJsonSchemaHelper.CreateBaseLayoutItemSchema();
+
+ // Build content and settings data schemas with constraints
+ JsonObject contentDataItemSchema = BlockJsonSchemaHelper.CreateContentDataSchema(config?.Blocks);
+ JsonObject settingsDataItemSchema = BlockJsonSchemaHelper.CreateSettingsDataSchema(config?.Blocks);
+
+ // Build expose schema (BlockList-specific)
+ JsonObject exposeItemSchema = BlockJsonSchemaHelper.CreateExposeItemSchema();
+
+ // Build the main schema
+ JsonObject schema = BlockJsonSchemaHelper.CreateRootSchema();
+ schema["properties"] = new JsonObject
+ {
+ ["layout"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ [Constants.PropertyEditors.Aliases.BlockList] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = layoutItemSchema,
+ },
+ },
+ },
+ ["contentData"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = contentDataItemSchema,
+ },
+ ["settingsData"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = settingsDataItemSchema,
+ },
+ ["expose"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = exposeItemSchema,
+ },
+ };
+
+ // Add validation constraints
+ var layoutArray = schema["properties"]!["layout"]!["properties"]![Constants.PropertyEditors.Aliases.BlockList]!.AsObject();
+ BlockJsonSchemaHelper.ApplyValidationConstraints(layoutArray, config?.ValidationLimit.Min, config?.ValidationLimit.Max);
+
+ return schema;
+ }
+
///
/// Instantiates a new for use with the block list editor property value editor.
///
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs
index 9f9606e3f019..e96a5f6487d1 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
@@ -14,7 +15,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.CheckBoxList,
ValueType = ValueTypes.Text, // We use the Text value type to ensure we don't run out of storage space in the database field with large lists with multiple values selected.
ValueEditorIsReusable = true)]
-public class CheckBoxListPropertyEditor : DataEditor
+public class CheckBoxListPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
@@ -30,6 +31,21 @@ public CheckBoxListPropertyEditor(IDataValueEditorFactory dataValueEditorFactory
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(IEnumerable);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("array", "null"),
+ ["items"] = new JsonObject
+ {
+ ["type"] = "string",
+ },
+ ["description"] = "Array of selected values",
+ };
+
///
protected override IConfigurationEditor CreateConfigurationEditor() =>
new ValueListConfigurationEditor(_ioHelper, _configurationEditorJsonSerializer);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs
index 15b53e691ef5..6379a036adee 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerPropertyEditor.cs
@@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataEditor(
Constants.PropertyEditors.Aliases.ColorPicker,
ValueEditorIsReusable = true)]
-public class ColorPickerPropertyEditor : DataEditor
+public class ColorPickerPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
@@ -38,6 +38,30 @@ public ColorPickerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory,
///
public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory();
+ ///
+ public Type? GetValueType(object? configuration) => typeof(JsonObject);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("object", "null"),
+ ["properties"] = new JsonObject
+ {
+ ["value"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Color value (hex format, e.g., '#ff0000')",
+ },
+ ["label"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Display label for the color",
+ },
+ },
+ ["description"] = "Color picker value with hex color and optional label",
+ };
+
///
protected override IDataValueEditor CreateValueEditor()
=> DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditor.cs
index 50d06b725bf1..2a6f5708195b 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditor.cs
@@ -4,6 +4,7 @@
using System.Data.SqlTypes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -21,7 +22,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.DateTime,
ValueType = ValueTypes.DateTime,
ValueEditorIsReusable = true)]
-public class DateTimePropertyEditor : DataEditor
+public class DateTimePropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -30,6 +31,18 @@ public DateTimePropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+ ///
+ public Type? GetValueType(object? configuration) => typeof(DateTime?);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "date-time",
+ ["description"] = "ISO 8601 date-time string",
+ };
+
///
protected override IDataValueEditor CreateValueEditor()
{
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorBase.cs
index b065d139490c..892ffb43da8e 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorBase.cs
@@ -3,6 +3,7 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
+using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
@@ -21,7 +22,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
///
/// Provides base functionality for date time property editors that store their value as a JSON string with timezone information.
///
-public abstract class DateTimePropertyEditorBase : DataEditor
+public abstract class DateTimePropertyEditorBase : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
private readonly IPropertyIndexValueFactory _propertyIndexValueFactory;
@@ -40,6 +41,30 @@ protected DateTimePropertyEditorBase(
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(DateTimeEditorValue);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("object", "null"),
+ ["properties"] = new JsonObject
+ {
+ ["date"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "ISO 8601 date-time string",
+ },
+ ["timeZone"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "IANA timezone identifier (e.g., 'Europe/London')",
+ },
+ },
+ ["description"] = "Date/time value with optional timezone",
+ };
+
///
protected override IConfigurationEditor CreateConfigurationEditor() =>
new DateTimeConfigurationEditor(_ioHelper);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexiblePropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexiblePropertyEditor.cs
index 9b5886f68592..df5591817b44 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexiblePropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexiblePropertyEditor.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
@@ -10,7 +11,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataEditor(
Constants.PropertyEditors.Aliases.DropDownListFlexible,
ValueEditorIsReusable = true)]
-public class DropDownFlexiblePropertyEditor : DataEditor
+public class DropDownFlexiblePropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
@@ -23,6 +24,21 @@ public DropDownFlexiblePropertyEditor(IDataValueEditorFactory dataValueEditorFac
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(IEnumerable);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("array", "null"),
+ ["items"] = new JsonObject
+ {
+ ["type"] = "string",
+ },
+ ["description"] = "Array of selected values from dropdown",
+ };
+
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/EmailAddressPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/EmailAddressPropertyEditor.cs
index 164bc6fd8545..bccf391bc036 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/EmailAddressPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/EmailAddressPropertyEditor.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Services;
@@ -13,7 +14,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataEditor(
Constants.PropertyEditors.Aliases.EmailAddress,
ValueEditorIsReusable = true)]
-public class EmailAddressPropertyEditor : DataEditor
+public class EmailAddressPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly ILocalizedTextService _localizedTextService;
@@ -27,6 +28,18 @@ public EmailAddressPropertyEditor(IDataValueEditorFactory dataValueEditorFactory
_localizedTextService = localizedTextService;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "email",
+ ["description"] = "Email address",
+ };
+
///
protected override IDataValueEditor CreateValueEditor()
{
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs
index 6dd536f32fb3..0b760aad3e92 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs
@@ -2,8 +2,10 @@
// See LICENSE for more details.
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -13,7 +15,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataEditor(
Constants.PropertyEditors.Aliases.UploadField,
ValueEditorIsReusable = true)]
-public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator
+public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
@@ -29,6 +31,32 @@ public FileUploadPropertyEditor(
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(FileUploadValue);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("object", "null"),
+ ["properties"] = new JsonObject
+ {
+ ["src"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Source file path",
+ },
+ ["temporaryFileId"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ ["description"] = "Temporary file ID for new uploads",
+ },
+ },
+ ["description"] = "File upload value with source path or temporary file reference",
+ };
+
///
public bool TryGetMediaPath(string? propertyEditorAlias, object? value, [MaybeNullWhen(false)] out string mediaPath)
{
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs
index c432486721de..b66589759219 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
@@ -26,6 +27,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
ValueEditorIsReusable = true)]
public class ImageCropperPropertyEditor : DataEditor,
IMediaUrlGenerator,
+ IValueSchemaProvider,
INotificationHandler,
INotificationHandler,
INotificationHandler,
@@ -72,6 +74,68 @@ public ImageCropperPropertyEditor(
public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory();
+ ///
+ public Type? GetValueType(object? configuration) => typeof(ImageCropperValue);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("object", "null"),
+ ["properties"] = new JsonObject
+ {
+ ["src"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Source image path",
+ },
+ ["temporaryFileId"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ ["description"] = "Temporary file ID for new uploads",
+ },
+ ["focalPoint"] = new JsonObject
+ {
+ ["type"] = new JsonArray("object", "null"),
+ ["properties"] = new JsonObject
+ {
+ ["left"] = new JsonObject { ["type"] = "number" },
+ ["top"] = new JsonObject { ["type"] = "number" },
+ },
+ ["description"] = "Focal point coordinates (0-1 range)",
+ },
+ ["crops"] = new JsonObject
+ {
+ ["type"] = new JsonArray("array", "null"),
+ ["items"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["alias"] = new JsonObject { ["type"] = new JsonArray("string", "null") },
+ ["width"] = new JsonObject { ["type"] = "integer" },
+ ["height"] = new JsonObject { ["type"] = "integer" },
+ ["coordinates"] = new JsonObject
+ {
+ ["type"] = new JsonArray("object", "null"),
+ ["properties"] = new JsonObject
+ {
+ ["x1"] = new JsonObject { ["type"] = "number" },
+ ["y1"] = new JsonObject { ["type"] = "number" },
+ ["x2"] = new JsonObject { ["type"] = "number" },
+ ["y2"] = new JsonObject { ["type"] = "number" },
+ },
+ },
+ },
+ },
+ ["description"] = "Image crop definitions",
+ },
+ },
+ ["description"] = "Image cropper value with source, focal point, and crop definitions",
+ };
+
public bool TryGetMediaPath(string? propertyEditorAlias, object? value, out string? mediaPath)
{
if (propertyEditorAlias == Alias &&
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
index 50fe850e04f2..ceaaefd2d215 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
@@ -24,7 +25,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.MediaPicker3,
ValueType = ValueTypes.Json,
ValueEditorIsReusable = true)]
-public class MediaPicker3PropertyEditor : DataEditor
+public class MediaPicker3PropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
@@ -41,6 +42,139 @@ public MediaPicker3PropertyEditor(IDataValueEditorFactory dataValueEditorFactory
///
public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory();
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string); // JSON string representation
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration)
+ {
+ var config = configuration as MediaPicker3Configuration;
+
+ // Build the item schema for individual media items
+ var itemSchema = new JsonObject
+ {
+ ["type"] = "object",
+ ["required"] = new JsonArray("key", "mediaKey"),
+ ["properties"] = new JsonObject
+ {
+ ["key"] = new JsonObject { ["type"] = "string", ["format"] = "uuid", ["pattern"] = ValueSchemaPatterns.Uuid },
+ ["mediaKey"] = new JsonObject { ["type"] = "string", ["format"] = "uuid", ["pattern"] = ValueSchemaPatterns.Uuid },
+ ["mediaTypeAlias"] = new JsonObject { ["type"] = "string" },
+ ["crops"] = BuildCropsSchema(config),
+ ["focalPoint"] = BuildFocalPointSchema(config),
+ },
+ };
+
+ // Build the array schema
+ var schema = new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("array", "null"),
+ ["items"] = itemSchema,
+ };
+
+ // Add min/max items constraints from configuration
+ if (config?.ValidationLimit.Min is int min && min > 0)
+ {
+ schema["minItems"] = min;
+ }
+
+ if (config?.ValidationLimit.Max is int max && max > 0)
+ {
+ schema["maxItems"] = max;
+ }
+
+ if (config?.Multiple == false)
+ {
+ schema["maxItems"] = 1;
+ }
+
+ return schema;
+ }
+
+ private static JsonObject BuildCropsSchema(MediaPicker3Configuration? config)
+ {
+ var cropItemSchema = new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["alias"] = new JsonObject { ["type"] = "string" },
+ ["width"] = new JsonObject { ["type"] = "integer" },
+ ["height"] = new JsonObject { ["type"] = "integer" },
+ ["coordinates"] = new JsonObject
+ {
+ ["oneOf"] = new JsonArray
+ {
+ new JsonObject { ["type"] = "null" },
+ new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["x1"] = new JsonObject { ["type"] = "number" },
+ ["y1"] = new JsonObject { ["type"] = "number" },
+ ["x2"] = new JsonObject { ["type"] = "number" },
+ ["y2"] = new JsonObject { ["type"] = "number" },
+ },
+ },
+ },
+ },
+ },
+ };
+
+ // If crops are configured, add enum constraint for crop aliases
+ if (config?.Crops is { Length: > 0 })
+ {
+ var cropAliases = new JsonArray();
+ foreach (var crop in config.Crops)
+ {
+ if (!string.IsNullOrEmpty(crop.Alias))
+ {
+ cropAliases.Add(JsonValue.Create(crop.Alias));
+ }
+ }
+
+ if (cropAliases.Count > 0)
+ {
+ var aliasProperty = cropItemSchema["properties"]!["alias"]!.AsObject();
+ aliasProperty["enum"] = cropAliases;
+ }
+ }
+
+ return new JsonObject
+ {
+ ["type"] = new JsonArray("array", "null"),
+ ["items"] = cropItemSchema,
+ };
+ }
+
+ private static JsonObject BuildFocalPointSchema(MediaPicker3Configuration? config)
+ {
+ // If focal point is disabled, always null
+ if (config?.EnableLocalFocalPoint == false)
+ {
+ return new JsonObject { ["type"] = "null" };
+ }
+
+ return new JsonObject
+ {
+ ["oneOf"] = new JsonArray
+ {
+ new JsonObject { ["type"] = "null" },
+ new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["left"] = new JsonObject { ["type"] = "number", ["minimum"] = 0, ["maximum"] = 1 },
+ ["top"] = new JsonObject { ["type"] = "number", ["minimum"] = 0, ["maximum"] = 1 },
+ },
+ },
+ },
+ };
+ }
+
///
protected override IConfigurationEditor CreateConfigurationEditor() =>
new MediaPicker3ConfigurationEditor(_ioHelper);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs
index 272117ac5680..79709222b042 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs
@@ -24,7 +24,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.MultiNodeTreePicker,
ValueType = ValueTypes.Text,
ValueEditorIsReusable = true)]
-public class MultiNodeTreePickerPropertyEditor : DataEditor
+public class MultiNodeTreePickerPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
@@ -38,6 +38,57 @@ public MultiNodeTreePickerPropertyEditor(IDataValueEditorFactory dataValueEditor
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(MultiNodeTreePickerPropertyValueEditor.EditorEntityReference[]);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration)
+ {
+ var schema = new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("array", "null"),
+ ["items"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["type"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["enum"] = new JsonArray("content", "media", "member"),
+ ["description"] = "Entity type (content, media, or member)",
+ },
+ ["unique"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ ["description"] = "GUID of the selected entity",
+ },
+ },
+ ["required"] = new JsonArray("type", "unique"),
+ },
+ ["description"] = "Array of selected entity references",
+ };
+
+ // Add minItems/maxItems from configuration if available
+ if (configuration is MultiNodePickerConfiguration pickerConfig)
+ {
+ if (pickerConfig.MinNumber > 0)
+ {
+ schema["minItems"] = pickerConfig.MinNumber;
+ }
+
+ if (pickerConfig.MaxNumber > 0)
+ {
+ schema["maxItems"] = pickerConfig.MaxNumber;
+ }
+ }
+
+ return schema;
+ }
+
///
protected override IConfigurationEditor CreateConfigurationEditor() =>
new MultiNodePickerConfigurationEditor(_ioHelper);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs
index 921d57d68ba0..1cc52890b066 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerPropertyEditor.cs
@@ -1,8 +1,10 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -10,7 +12,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.MultiUrlPicker,
ValueType = ValueTypes.Json,
ValueEditorIsReusable = true)]
-public class MultiUrlPickerPropertyEditor : DataEditor
+public class MultiUrlPickerPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
@@ -23,6 +25,76 @@ public MultiUrlPickerPropertyEditor(IIOHelper ioHelper, IDataValueEditorFactory
public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory();
+ ///
+ public Type? GetValueType(object? configuration) => typeof(LinkDisplay[]);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration)
+ {
+ var schema = new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("array", "null"),
+ ["items"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ ["name"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Display name of the link",
+ },
+ ["target"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Target attribute (e.g., '_blank')",
+ },
+ ["type"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["enum"] = new JsonArray("document", "media", "external", null),
+ ["description"] = "Link type (document, media, or external)",
+ },
+ ["unique"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["format"] = "uuid",
+ ["pattern"] = ValueSchemaPatterns.Uuid,
+ ["description"] = "GUID of linked content/media (for document/media types)",
+ },
+ ["url"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "URL (for external links)",
+ },
+ ["queryString"] = new JsonObject
+ {
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Query string portion of the URL",
+ },
+ },
+ },
+ ["description"] = "Array of link objects",
+ };
+
+ // Add minItems/maxItems from configuration if available
+ if (configuration is MultiUrlPickerConfiguration pickerConfig)
+ {
+ if (pickerConfig.MinNumber > 0)
+ {
+ schema["minItems"] = pickerConfig.MinNumber;
+ }
+
+ if (pickerConfig.MaxNumber > 0)
+ {
+ schema["maxItems"] = pickerConfig.MaxNumber;
+ }
+ }
+
+ return schema;
+ }
+
protected override IConfigurationEditor CreateConfigurationEditor() =>
new MultiUrlPickerConfigurationEditor(_ioHelper);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs
index d76ff2b0ab19..24d4fe0c7b9e 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
@@ -22,7 +23,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.MultipleTextstring,
ValueType = ValueTypes.Text,
ValueEditorIsReusable = true)]
-public class MultipleTextStringPropertyEditor : DataEditor
+public class MultipleTextStringPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
@@ -36,6 +37,40 @@ public MultipleTextStringPropertyEditor(IIOHelper ioHelper, IDataValueEditorFact
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(IEnumerable);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration)
+ {
+ var schema = new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("array", "null"),
+ ["items"] = new JsonObject
+ {
+ ["type"] = "string",
+ },
+ ["description"] = "Array of text strings",
+ };
+
+ // Add min/max items from configuration if available
+ if (configuration is MultipleTextStringConfiguration textStringConfig)
+ {
+ if (textStringConfig.Min > 0)
+ {
+ schema["minItems"] = textStringConfig.Min;
+ }
+
+ if (textStringConfig.Max > 0)
+ {
+ schema["maxItems"] = textStringConfig.Max;
+ }
+ }
+
+ return schema;
+ }
+
///
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs
index cf2c4ad3fa58..a33d78a01532 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Validation;
@@ -19,7 +20,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.RadioButtonList,
ValueType = ValueTypes.String,
ValueEditorIsReusable = true)]
-public class RadioButtonsPropertyEditor : DataEditor
+public class RadioButtonsPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
@@ -35,8 +36,18 @@ public RadioButtonsPropertyEditor(IDataValueEditorFactory dataValueEditorFactory
SupportsReadOnly = true;
}
- ///
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ ["description"] = "Selected value from the radio button list",
+ };
+ ///
protected override IConfigurationEditor CreateConfigurationEditor() =>
new ValueListConfigurationEditor(_ioHelper, _configurationEditorJsonSerializer);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
index 89415fb2589a..6d2065a7ba62 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PropertyEditors;
@@ -17,6 +18,7 @@
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Templates;
using Umbraco.Cms.Infrastructure.Extensions;
+using Umbraco.Cms.Infrastructure.PropertyEditors;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -28,7 +30,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.RichText,
ValueType = ValueTypes.Text,
ValueEditorIsReusable = true)]
-public class RichTextPropertyEditor : DataEditor
+public class RichTextPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
private readonly IRichTextPropertyIndexValueFactory _richTextPropertyIndexValueFactory;
@@ -55,6 +57,75 @@ public RichTextPropertyEditor(
public override bool SupportsConfigurableElements => true;
+ ///
+ public Type? GetValueType(object? configuration) => typeof(RichTextEditorValue);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration)
+ {
+ var config = configuration as RichTextConfiguration;
+
+ // Build schemas using helper
+ JsonObject layoutItemSchema = BlockJsonSchemaHelper.CreateBaseLayoutItemSchema();
+ JsonObject contentDataItemSchema = BlockJsonSchemaHelper.CreateContentDataSchema(config?.Blocks);
+ JsonObject settingsDataItemSchema = BlockJsonSchemaHelper.CreateSettingsDataSchema(config?.Blocks);
+ JsonObject exposeItemSchema = BlockJsonSchemaHelper.CreateExposeItemSchema();
+
+ // Build blocks schema
+ var blocksSchema = new JsonObject
+ {
+ ["type"] = new JsonArray("object", "null"),
+ ["properties"] = new JsonObject
+ {
+ ["layout"] = new JsonObject
+ {
+ ["type"] = "object",
+ ["properties"] = new JsonObject
+ {
+ [Constants.PropertyEditors.Aliases.RichText] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = layoutItemSchema,
+ },
+ },
+ },
+ ["contentData"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = contentDataItemSchema,
+ },
+ ["settingsData"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = settingsDataItemSchema,
+ },
+ ["expose"] = new JsonObject
+ {
+ ["type"] = "array",
+ ["items"] = exposeItemSchema,
+ },
+ },
+ };
+
+ // Build main schema
+ return new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("object", "null"),
+ ["properties"] = new JsonObject
+ {
+ ["markup"] = new JsonObject
+ {
+ ["type"] = "string",
+ ["description"] = "HTML markup content",
+ },
+ ["blocks"] = blocksSchema,
+ },
+ ["required"] = new JsonArray("markup"),
+ ["description"] = "Rich text editor value with HTML markup and optional blocks",
+ };
+ }
+
///
public override bool CanMergePartialPropertyValues(IPropertyType propertyType) => propertyType.VariesByCulture() is false;
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs
index 7f38484f58d0..7e07ebb947a7 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs
@@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataEditor(
Constants.PropertyEditors.Aliases.Slider,
ValueEditorIsReusable = true)]
-public class SliderPropertyEditor : DataEditor
+public class SliderPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
@@ -37,6 +37,55 @@ public SliderPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIOH
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(SliderPropertyValueEditor.SliderRange);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration)
+ {
+ var schema = new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("object", "null"),
+ ["properties"] = new JsonObject
+ {
+ ["from"] = new JsonObject
+ {
+ ["type"] = "number",
+ ["description"] = "Slider range start value",
+ },
+ ["to"] = new JsonObject
+ {
+ ["type"] = "number",
+ ["description"] = "Slider range end value (same as 'from' for single-value slider)",
+ },
+ },
+ ["required"] = new JsonArray("from", "to"),
+ ["description"] = "Slider value with from/to range",
+ };
+
+ // Add min/max constraints from configuration if available
+ if (configuration is SliderConfiguration sliderConfig)
+ {
+ var fromSchema = (JsonObject)schema["properties"]!["from"]!;
+ var toSchema = (JsonObject)schema["properties"]!["to"]!;
+
+ if (sliderConfig.MinimumValue != 0)
+ {
+ fromSchema["minimum"] = sliderConfig.MinimumValue;
+ toSchema["minimum"] = sliderConfig.MinimumValue;
+ }
+
+ if (sliderConfig.MaximumValue != 0)
+ {
+ fromSchema["maximum"] = sliderConfig.MaximumValue;
+ toSchema["maximum"] = sliderConfig.MaximumValue;
+ }
+ }
+
+ return schema;
+ }
+
///
protected override IDataValueEditor CreateValueEditor()
=> DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs
index ab56bc14ee00..4227761b68de 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
@@ -22,7 +23,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.Tags,
ValueEditorIsReusable = true,
ValueType = ValueTypes.Text)]
-public class TagsPropertyEditor : DataEditor
+public class TagsPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly ITagPropertyIndexValueFactory _tagPropertyIndexValueFactory;
private readonly IIOHelper _ioHelper;
@@ -39,6 +40,21 @@ public TagsPropertyEditor(
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _tagPropertyIndexValueFactory;
+ ///
+ public Type? GetValueType(object? configuration) => typeof(IEnumerable);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("array", "null"),
+ ["items"] = new JsonObject
+ {
+ ["type"] = "string",
+ },
+ ["description"] = "Array of tag values",
+ };
+
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TextAreaPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TextAreaPropertyEditor.cs
index a5c8c8627dba..42b5a3b48e98 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/TextAreaPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/TextAreaPropertyEditor.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
@@ -13,7 +14,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.TextArea,
ValueType = ValueTypes.Text,
ValueEditorIsReusable = true)]
-public class TextAreaPropertyEditor : DataEditor
+public class TextAreaPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
@@ -27,6 +28,35 @@ public TextAreaPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, II
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration)
+ {
+ var schema = new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ };
+
+ // Add maxLength constraint from configuration if available
+ if (configuration is TextAreaConfiguration textAreaConfig && textAreaConfig.MaxChars > 0)
+ {
+ schema["maxLength"] = textAreaConfig.MaxChars;
+ }
+ else if (configuration is IDictionary configDict &&
+ configDict.TryGetValue("maxChars", out var maxCharsValue))
+ {
+ if (maxCharsValue is int maxChars && maxChars > 0)
+ {
+ schema["maxLength"] = maxChars;
+ }
+ }
+
+ return schema;
+ }
+
///
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TextboxPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TextboxPropertyEditor.cs
index 734bd539329b..348e2b29e941 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/TextboxPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/TextboxPropertyEditor.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
@@ -12,7 +13,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataEditor(
Constants.PropertyEditors.Aliases.TextBox,
ValueEditorIsReusable = true)]
-public class TextboxPropertyEditor : DataEditor
+public class TextboxPropertyEditor : DataEditor, IValueSchemaProvider
{
private readonly IIOHelper _ioHelper;
@@ -26,6 +27,35 @@ public TextboxPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIO
SupportsReadOnly = true;
}
+ ///
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration)
+ {
+ var schema = new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = new JsonArray("string", "null"),
+ };
+
+ // Add maxLength constraint from configuration if available
+ if (configuration is TextboxConfiguration textboxConfig && textboxConfig.MaxChars > 0)
+ {
+ schema["maxLength"] = textboxConfig.MaxChars;
+ }
+ else if (configuration is IDictionary configDict &&
+ configDict.TryGetValue("maxChars", out var maxCharsValue))
+ {
+ if (maxCharsValue is int maxChars && maxChars > 0)
+ {
+ schema["maxLength"] = maxChars;
+ }
+ }
+
+ return schema;
+ }
+
///
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs
index 705b51befde2..bd6c128cc413 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -17,7 +18,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.Boolean,
ValueType = ValueTypes.Integer,
ValueEditorIsReusable = true)]
-public class TrueFalsePropertyEditor : DataEditor
+public class TrueFalsePropertyEditor : DataEditor, IValueSchemaProvider
{
///
/// Initializes a new instance of the class.
@@ -26,6 +27,17 @@ public TrueFalsePropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
+ ///
+ public Type? GetValueType(object? configuration) => typeof(bool);
+
+ ///
+ public JsonObject? GetValueSchema(object? configuration) => new()
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = "boolean",
+ ["description"] = "Boolean value (true or false)",
+ };
+
///
protected override IDataValueEditor CreateValueEditor()
=> DataValueEditorFactory.Create(Attribute!);
diff --git a/src/Umbraco.Infrastructure/Services/Implement/PropertyEditorSchemaService.cs b/src/Umbraco.Infrastructure/Services/Implement/PropertyEditorSchemaService.cs
new file mode 100644
index 000000000000..d3e0635ec61f
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Services/Implement/PropertyEditorSchemaService.cs
@@ -0,0 +1,80 @@
+using System.Text.Json.Nodes;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+
+namespace Umbraco.Cms.Infrastructure.Services.Implement;
+
+///
+/// Provides services for querying property editor value schemas.
+///
+internal sealed class PropertyEditorSchemaService : IPropertyEditorSchemaService
+{
+ private readonly IDataTypeService _dataTypeService;
+ private readonly DataEditorCollection _dataEditors;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PropertyEditorSchemaService(
+ IDataTypeService dataTypeService,
+ DataEditorCollection dataEditors)
+ {
+ _dataTypeService = dataTypeService;
+ _dataEditors = dataEditors;
+ }
+
+ ///
+ public async Task> GetSchemaAsync(Guid dataTypeKey)
+ {
+ var dataType = await _dataTypeService.GetAsync(dataTypeKey);
+ if (dataType is null)
+ {
+ return Attempt.FailWithStatus(PropertyEditorSchemaOperationStatus.DataTypeNotFound, new PropertyValueSchema(null, null));
+ }
+
+ IValueSchemaProvider? provider = GetSchemaProvider(dataType.EditorAlias);
+ if (provider is null)
+ {
+ return Attempt.FailWithStatus(PropertyEditorSchemaOperationStatus.SchemaNotSupported, new PropertyValueSchema(null, null));
+ }
+
+ Type? valueType = GetValueTypeFromProvider(provider, dataType.ConfigurationObject);
+ JsonObject? jsonSchema = GetValueSchemaFromProvider(provider, dataType.ConfigurationObject);
+
+ return Attempt.SucceedWithStatus(
+ PropertyEditorSchemaOperationStatus.Success,
+ new PropertyValueSchema(valueType, jsonSchema));
+ }
+
+ ///
+ public Type? GetValueType(string propertyEditorAlias, object? configuration)
+ {
+ IValueSchemaProvider? provider = GetSchemaProvider(propertyEditorAlias);
+ return provider is not null ? GetValueTypeFromProvider(provider, configuration) : null;
+ }
+
+ ///
+ public JsonObject? GetValueSchema(string propertyEditorAlias, object? configuration)
+ {
+ IValueSchemaProvider? provider = GetSchemaProvider(propertyEditorAlias);
+ return provider is not null ? GetValueSchemaFromProvider(provider, configuration) : null;
+ }
+
+ ///
+ public bool SupportsSchema(string propertyEditorAlias)
+ => GetSchemaProvider(propertyEditorAlias) is not null;
+
+ private static Type? GetValueTypeFromProvider(IValueSchemaProvider provider, object? configuration)
+ => provider.GetValueType(configuration);
+
+ private static JsonObject? GetValueSchemaFromProvider(IValueSchemaProvider provider, object? configuration)
+ => provider.GetValueSchema(configuration);
+
+ private IValueSchemaProvider? GetSchemaProvider(string propertyEditorAlias)
+ {
+ IDataEditor? editor = _dataEditors.FirstOrDefault(e => e.Alias == propertyEditorAlias);
+ return editor as IValueSchemaProvider;
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/PropertyEditorSchemaServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/PropertyEditorSchemaServiceTests.cs
new file mode 100644
index 000000000000..713b0727eea9
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/PropertyEditorSchemaServiceTests.cs
@@ -0,0 +1,115 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+using Umbraco.Cms.Tests.Common.Testing;
+using Umbraco.Cms.Tests.Integration.Testing;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
+
+///
+/// Integration tests for the .
+///
+[TestFixture]
+[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
+internal sealed class PropertyEditorSchemaServiceTests : UmbracoIntegrationTest
+{
+ private IPropertyEditorSchemaService PropertyEditorSchemaService => GetRequiredService();
+
+ private IDataTypeService DataTypeService => GetRequiredService();
+
+ private IDataValueEditorFactory DataValueEditorFactory => GetRequiredService();
+
+ private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer =>
+ GetRequiredService();
+
+ [Test]
+ public void SupportsSchema_Returns_True_For_Integer_Editor()
+ {
+ // Act
+ var result = PropertyEditorSchemaService.SupportsSchema(Constants.PropertyEditors.Aliases.Integer);
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void SupportsSchema_Returns_True_For_ContentPicker_Editor()
+ {
+ // Act
+ var result = PropertyEditorSchemaService.SupportsSchema(Constants.PropertyEditors.Aliases.ContentPicker);
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public async Task GetSchemaAsync_Returns_Success_For_Integer_DataType()
+ {
+ // Arrange
+ var dataType = new DataType(
+ new IntegerPropertyEditor(DataValueEditorFactory),
+ ConfigurationEditorJsonSerializer)
+ {
+ Name = "Test Integer GetSchemaAsync",
+ DatabaseType = ValueStorageType.Integer,
+ ConfigurationData = new Dictionary
+ {
+ { "min", 0 },
+ { "max", 100 },
+ },
+ };
+ var createResult = await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey);
+ Assert.That(createResult.Success, Is.True);
+
+ // Act
+ var result = await PropertyEditorSchemaService.GetSchemaAsync(dataType.Key);
+
+ // Assert
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.Status, Is.EqualTo(PropertyEditorSchemaOperationStatus.Success));
+ Assert.That(result.Result.ValueType, Is.EqualTo(typeof(int?)));
+ Assert.That(result.Result.JsonSchema, Is.Not.Null);
+ Assert.That(result.Result.JsonSchema!["minimum"]?.GetValue(), Is.EqualTo(0));
+ Assert.That(result.Result.JsonSchema["maximum"]?.GetValue(), Is.EqualTo(100));
+ }
+
+ [Test]
+ public async Task GetSchemaAsync_Returns_DataTypeNotFound_For_NonExistent_DataType()
+ {
+ // Act
+ var result = await PropertyEditorSchemaService.GetSchemaAsync(Guid.NewGuid());
+
+ // Assert
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.Status, Is.EqualTo(PropertyEditorSchemaOperationStatus.DataTypeNotFound));
+ }
+
+ [Test]
+ public async Task GetSchemaAsync_Returns_SchemaNotSupported_For_Editor_Without_Schema()
+ {
+ // Arrange - Void editor doesn't implement IValueSchemaProvider
+ var dataType = new DataType(
+ new VoidEditor(DataValueEditorFactory),
+ ConfigurationEditorJsonSerializer)
+ {
+ Name = "Test Void GetSchemaAsync",
+ DatabaseType = ValueStorageType.Nvarchar,
+ };
+ var createResult = await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey);
+ Assert.That(createResult.Success, Is.True);
+
+ // Act
+ var result = await PropertyEditorSchemaService.GetSchemaAsync(dataType.Key);
+
+ // Assert
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.Status, Is.EqualTo(PropertyEditorSchemaOperationStatus.SchemaNotSupported));
+ }
+}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueSchemaProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueSchemaProviderTests.cs
new file mode 100644
index 000000000000..33f0f9f60fce
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueSchemaProviderTests.cs
@@ -0,0 +1,144 @@
+using System.Text.Json.Nodes;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.IO;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Strings;
+
+namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
+
+[TestFixture]
+public class ValueSchemaProviderTests
+{
+ [Test]
+ public void IntegerPropertyEditor_Returns_Integer_Schema()
+ {
+ // Arrange
+ var editor = CreateIntegerPropertyEditor();
+
+ // Act
+ var schema = editor.GetValueSchema(null);
+
+ // Assert
+ Assert.That(schema, Is.Not.Null);
+ Assert.That(schema!["$schema"]?.GetValue(), Is.EqualTo("https://json-schema.org/draft/2020-12/schema"));
+
+ var typeArray = schema["type"] as JsonArray;
+ Assert.That(typeArray, Is.Not.Null);
+ Assert.That(typeArray!.Select(t => t?.GetValue()), Is.EquivalentTo(new[] { "integer", "null" }));
+ }
+
+ [Test]
+ public void IntegerPropertyEditor_Returns_ValueType()
+ {
+ // Arrange
+ var editor = CreateIntegerPropertyEditor();
+
+ // Act
+ var valueType = editor.GetValueType(null);
+
+ // Assert
+ Assert.That(valueType, Is.EqualTo(typeof(int?)));
+ }
+
+ [Test]
+ public void IntegerPropertyEditor_Includes_MinMax_When_Configured()
+ {
+ // Arrange
+ var editor = CreateIntegerPropertyEditor();
+ var config = new Dictionary
+ {
+ { "min", 10 },
+ { "max", 100 },
+ };
+
+ // Act
+ var schema = editor.GetValueSchema(config);
+
+ // Assert
+ Assert.That(schema, Is.Not.Null);
+ Assert.That(schema!["minimum"]?.GetValue(), Is.EqualTo(10));
+ Assert.That(schema["maximum"]?.GetValue(), Is.EqualTo(100));
+ }
+
+ [Test]
+ public void IntegerPropertyEditor_Includes_Step_When_Greater_Than_One()
+ {
+ // Arrange
+ var editor = CreateIntegerPropertyEditor();
+ var config = new Dictionary
+ {
+ { "min", 0 },
+ { "step", 5 },
+ };
+
+ // Act
+ var schema = editor.GetValueSchema(config);
+
+ // Assert
+ Assert.That(schema, Is.Not.Null);
+ Assert.That(schema!["multipleOf"]?.GetValue(), Is.EqualTo(5));
+ }
+
+ [Test]
+ public void IntegerPropertyEditor_Omits_Step_When_One()
+ {
+ // Arrange
+ var editor = CreateIntegerPropertyEditor();
+ var config = new Dictionary
+ {
+ { "step", 1 },
+ };
+
+ // Act
+ var schema = editor.GetValueSchema(config);
+
+ // Assert
+ Assert.That(schema, Is.Not.Null);
+ Assert.That(schema!.ContainsKey("multipleOf"), Is.False);
+ }
+
+ [Test]
+ public void ContentPickerPropertyEditor_Returns_ValueType_String()
+ {
+ // Arrange
+ var editor = CreateContentPickerPropertyEditor();
+
+ // Act
+ var valueType = editor.GetValueType(null);
+
+ // Assert
+ Assert.That(valueType, Is.EqualTo(typeof(string)));
+ }
+
+ private static IntegerPropertyEditor CreateIntegerPropertyEditor()
+ {
+ var dataValueEditorFactory = Mock.Of(f =>
+ f.Create(It.IsAny()) ==
+ new DataValueEditor(
+ Mock.Of(),
+ Mock.Of(),
+ Mock.Of(),
+ new DataEditorAttribute(Constants.PropertyEditors.Aliases.Integer)));
+
+ return new IntegerPropertyEditor(dataValueEditorFactory);
+ }
+
+ private static ContentPickerPropertyEditor CreateContentPickerPropertyEditor()
+ {
+ var dataValueEditorFactory = Mock.Of(f =>
+ f.Create(It.IsAny()) ==
+ new DataValueEditor(
+ Mock.Of(),
+ Mock.Of(),
+ Mock.Of(),
+ new DataEditorAttribute(Constants.PropertyEditors.Aliases.ContentPicker)));
+
+ return new ContentPickerPropertyEditor(
+ dataValueEditorFactory,
+ Mock.Of());
+ }
+}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyEditorSchemaServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyEditorSchemaServiceTests.cs
new file mode 100644
index 000000000000..ec04ca78a4e0
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyEditorSchemaServiceTests.cs
@@ -0,0 +1,235 @@
+using System.Text.Json.Nodes;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.IO;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.OperationStatus;
+using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Infrastructure.Services.Implement;
+
+namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Services;
+
+[TestFixture]
+public partial class PropertyEditorSchemaServiceTests
+{
+ private Mock _dataTypeServiceMock = null!;
+ private List _dataEditors = null!;
+ private PropertyEditorSchemaService _sut = null!;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _dataTypeServiceMock = new Mock();
+ _dataEditors = [];
+ var dataEditorCollection = new DataEditorCollection(() => _dataEditors);
+ _sut = new PropertyEditorSchemaService(_dataTypeServiceMock.Object, dataEditorCollection);
+ }
+
+ [Test]
+ public void SupportsSchema_Returns_True_For_Editor_Implementing_IValueSchemaProvider()
+ {
+ // Arrange
+ var schemaProviderEditor = new MockSchemaProviderEditor();
+ SetupDataEditors(schemaProviderEditor);
+
+ // Act
+ var result = _sut.SupportsSchema("test.schemaProvider");
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void SupportsSchema_Returns_False_For_Editor_Not_Implementing_IValueSchemaProvider()
+ {
+ // Arrange
+ var regularEditor = new MockRegularEditor();
+ SetupDataEditors(regularEditor);
+
+ // Act
+ var result = _sut.SupportsSchema("test.regular");
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void SupportsSchema_Returns_False_For_Unknown_Editor()
+ {
+ // Arrange
+ SetupDataEditors();
+
+ // Act
+ var result = _sut.SupportsSchema("unknown.editor");
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void GetValueType_Returns_Type_From_Provider()
+ {
+ // Arrange
+ var schemaProviderEditor = new MockSchemaProviderEditor();
+ SetupDataEditors(schemaProviderEditor);
+
+ // Act
+ var result = _sut.GetValueType("test.schemaProvider", null);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(typeof(string)));
+ }
+
+ [Test]
+ public void GetValueType_Returns_Null_For_NonProvider_Editor()
+ {
+ // Arrange
+ var regularEditor = new MockRegularEditor();
+ SetupDataEditors(regularEditor);
+
+ // Act
+ var result = _sut.GetValueType("test.regular", null);
+
+ // Assert
+ Assert.That(result, Is.Null);
+ }
+
+ [Test]
+ public void GetValueSchema_Returns_Schema_From_Provider()
+ {
+ // Arrange
+ var schemaProviderEditor = new MockSchemaProviderEditor();
+ SetupDataEditors(schemaProviderEditor);
+
+ // Act
+ var result = _sut.GetValueSchema("test.schemaProvider", null);
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result!["type"]?.GetValue(), Is.EqualTo("string"));
+ }
+
+ [Test]
+ public void GetValueSchema_Returns_Null_For_NonProvider_Editor()
+ {
+ // Arrange
+ var regularEditor = new MockRegularEditor();
+ SetupDataEditors(regularEditor);
+
+ // Act
+ var result = _sut.GetValueSchema("test.regular", null);
+
+ // Assert
+ Assert.That(result, Is.Null);
+ }
+
+ [Test]
+ public async Task GetSchemaAsync_Returns_Success_With_Both_Type_And_Schema()
+ {
+ // Arrange
+ var dataTypeKey = Guid.NewGuid();
+ var schemaProviderEditor = new MockSchemaProviderEditor();
+ SetupDataEditors(schemaProviderEditor);
+ SetupDataType(dataTypeKey, "test.schemaProvider");
+
+ // Act
+ var result = await _sut.GetSchemaAsync(dataTypeKey);
+
+ // Assert
+ Assert.That(result.Success, Is.True);
+ Assert.That(result.Status, Is.EqualTo(PropertyEditorSchemaOperationStatus.Success));
+ Assert.That(result.Result.ValueType, Is.EqualTo(typeof(string)));
+ Assert.That(result.Result.JsonSchema, Is.Not.Null);
+ Assert.That(result.Result.JsonSchema!["type"]?.GetValue(), Is.EqualTo("string"));
+ }
+
+ [Test]
+ public async Task GetSchemaAsync_Returns_DataTypeNotFound_When_DataType_Not_Found()
+ {
+ // Arrange
+ var dataTypeKey = Guid.NewGuid();
+ _dataTypeServiceMock.Setup(x => x.GetAsync(dataTypeKey)).ReturnsAsync((IDataType?)null);
+
+ // Act
+ var result = await _sut.GetSchemaAsync(dataTypeKey);
+
+ // Assert
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.Status, Is.EqualTo(PropertyEditorSchemaOperationStatus.DataTypeNotFound));
+ }
+
+ [Test]
+ public async Task GetSchemaAsync_Returns_SchemaNotSupported_When_Editor_Does_Not_Support_Schema()
+ {
+ // Arrange
+ var dataTypeKey = Guid.NewGuid();
+ var regularEditor = new MockRegularEditor();
+ SetupDataEditors(regularEditor);
+ SetupDataType(dataTypeKey, "test.regular");
+
+ // Act
+ var result = await _sut.GetSchemaAsync(dataTypeKey);
+
+ // Assert
+ Assert.That(result.Success, Is.False);
+ Assert.That(result.Status, Is.EqualTo(PropertyEditorSchemaOperationStatus.SchemaNotSupported));
+ }
+
+ private void SetupDataEditors(params IDataEditor[] editors)
+ {
+ _dataEditors.Clear();
+ _dataEditors.AddRange(editors);
+ }
+
+ private void SetupDataType(Guid key, string editorAlias, object? configuration = null)
+ {
+ var dataType = Mock.Of(dt =>
+ dt.Key == key &&
+ dt.EditorAlias == editorAlias &&
+ dt.ConfigurationObject == configuration);
+
+ _dataTypeServiceMock.Setup(x => x.GetAsync(key)).ReturnsAsync(dataType);
+ }
+
+ private class MockSchemaProviderEditor : IDataEditor, IValueSchemaProvider
+ {
+ public string Alias => "test.schemaProvider";
+ public string Name => "Test Schema Provider";
+ public string Icon => "icon-test";
+ public string? Group => null;
+ public bool IsDeprecated => false;
+ public IDictionary? DefaultConfiguration => null;
+ public IPropertyIndexValueFactory PropertyIndexValueFactory => null!;
+
+ public IDataValueEditor GetValueEditor() => null!;
+ public IDataValueEditor GetValueEditor(object? configuration) => null!;
+ public IConfigurationEditor GetConfigurationEditor() => null!;
+
+ public Type? GetValueType(object? configuration) => typeof(string);
+
+ public JsonObject? GetValueSchema(object? configuration) => new JsonObject
+ {
+ ["$schema"] = "https://json-schema.org/draft/2020-12/schema",
+ ["type"] = "string",
+ };
+ }
+
+ private class MockRegularEditor : IDataEditor
+ {
+ public string Alias => "test.regular";
+ public string Name => "Test Regular";
+ public string Icon => "icon-test";
+ public string? Group => null;
+ public bool IsDeprecated => false;
+ public IDictionary? DefaultConfiguration => null;
+ public IPropertyIndexValueFactory PropertyIndexValueFactory => null!;
+
+ public IDataValueEditor GetValueEditor() => null!;
+ public IDataValueEditor GetValueEditor(object? configuration) => null!;
+ public IConfigurationEditor GetConfigurationEditor() => null!;
+ }
+}