Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d2441d7
Basic implementaion
Migaroez Feb 4, 2026
8d8b8cf
Tests and schema validation
Migaroez Feb 5, 2026
25972a2
Attemp refactor
Migaroez Feb 5, 2026
1a7b745
Fix json single parent bug
Migaroez Feb 10, 2026
a68d392
Surface doctype schema validation to management api
Migaroez Feb 10, 2026
a520af8
Improve block schema and make validation errors less verbose
Migaroez Feb 10, 2026
f647403
fix validation error cleanup
Migaroez Feb 10, 2026
d6fd40d
Improved GUID handling | added schema for all propertyEditors
Migaroez Feb 12, 2026
697c101
Add ContentTypeInputSchema
Migaroez Feb 12, 2026
55bab09
move contenttype schemas to be actual jsonschemas
Migaroez Feb 13, 2026
c206e73
Fix block limit on blocklist and grid
Migaroez Feb 16, 2026
1509980
add datatype schema batch
Migaroez Feb 16, 2026
a2d019a
Refactoring blocks json schema generation and add to richtext
Migaroez Feb 16, 2026
7e2394f
Package version update and more tests!
Migaroez Feb 17, 2026
bdb9dd5
ConvertToJsonNode optimization
Migaroez Feb 17, 2026
225f0e7
async refactor
Migaroez Feb 17, 2026
57eae20
Add editorUiAlias to x-umbraco-properties and make DataType ref route…
Migaroez Feb 17, 2026
fd96649
Removed JsonSchema.net due to possible license issues
Migaroez Feb 18, 2026
93c3635
Merge branch 'main' into v17/task/property-editor-schemas
nielslyngsoe Mar 10, 2026
c27e287
Merge branch 'main' into v17/task/property-editor-schemas
Migaroez Mar 11, 2026
2baa4fb
Use void editor in the noop schema test
Migaroez Mar 11, 2026
36300a7
Merge branch 'v17/task/property-editor-schemas' of https://github.com…
Migaroez Mar 11, 2026
17dd7c1
Merge branch 'main' into v17/task/property-editor-schemas
AndyButland Mar 11, 2026
7a268b6
Cleanup leftovers from Schema validation removal
Migaroez Mar 11, 2026
b23b94b
Move batch logic into batchcontroller
Migaroez Mar 11, 2026
903036b
Update src/Umbraco.Infrastructure/PropertyEditors/BlockJsonSchemaHelp…
Migaroez Mar 11, 2026
7ba2085
Update src/Umbraco.Cms.Api.Management/Services/ContentTypeJsonSchemaS…
Migaroez Mar 11, 2026
5b596c7
Merge branch 'v17/task/property-editor-schemas' of https://github.com…
Migaroez Mar 11, 2026
3599c5b
Merge branch 'v17/task/property-editor-schemas' of https://github.com…
AndyButland Mar 12, 2026
a5a4cfa
Fixed build error.
AndyButland Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,4 @@
<!-- TODO (V19): Remove these pinned dependencies when the Markdown dependency is removed. -->
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Controller for retrieving multiple data type value schemas in a single request.
/// </summary>
[ApiVersion("1.0")]
public class BatchSchemasDataTypeController : DataTypeControllerBase
{
private readonly IPropertyEditorSchemaService _schemaService;

/// <summary>
/// Initializes a new instance of the <see cref="BatchSchemasDataTypeController"/> class.
/// </summary>
/// <param name="schemaService">The property editor schema service.</param>
public BatchSchemasDataTypeController(IPropertyEditorSchemaService schemaService)
=> _schemaService = schemaService;

/// <summary>
/// Gets the value schemas for multiple data types.
/// </summary>
/// <param name="cancellationToken">A cancellation token.</param>
/// <param name="ids">The unique identifiers of the data types.</param>
/// <returns>The schema information for the requested data types.</returns>
/// <remarks>
/// Returns schema information for property editors that implement <c>IValueSchemaProvider</c>.
/// Each item includes an error field if the schema could not be retrieved (e.g., data type not found or schema not supported).
/// </remarks>
[HttpGet("schemas/batch")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(FetchResponseModel<DataTypeSchemaItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetSchemas(
CancellationToken cancellationToken,
[FromQuery(Name = "id")] Guid[] ids)
{
Guid[] requestedIds = [.. ids.Distinct()];

if (requestedIds.Length == 0)
{
return Ok(new FetchResponseModel<DataTypeSchemaItemResponseModel>());
}

var items = new List<DataTypeSchemaItemResponseModel>();

foreach (Guid id in requestedIds)
{
Attempt<PropertyValueSchema, PropertyEditorSchemaOperationStatus> 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(),
});
}
Comment thread
AndyButland marked this conversation as resolved.

return Ok(new FetchResponseModel<DataTypeSchemaItemResponseModel>
{
Total = items.Count,
Items = items,
});
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Comment thread
AndyButland marked this conversation as resolved.
.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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Controller for retrieving data type value schemas.
/// </summary>
[ApiVersion("1.0")]
public class SchemaDataTypeController : DataTypeControllerBase
{
private readonly IPropertyEditorSchemaService _schemaService;

/// <summary>
/// Initializes a new instance of the <see cref="SchemaDataTypeController"/> class.
/// </summary>
/// <param name="schemaService">The property editor schema service.</param>
public SchemaDataTypeController(IPropertyEditorSchemaService schemaService)
=> _schemaService = schemaService;

/// <summary>
/// Gets the value schema for a data type.
/// </summary>
/// <param name="id">The unique identifier of the data type.</param>
/// <returns>The schema information for the data type's values.</returns>
/// <remarks>
/// Returns schema information for property editors that implement <c>IValueSchemaProvider</c>.
/// Returns 404 if the data type is not found or doesn't support schema information.
/// </remarks>
[HttpGet("{id:guid}/schema")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DataTypeSchemaResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Schema(Guid id)
{
Attempt<PropertyValueSchema, PropertyEditorSchemaOperationStatus> 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,
});
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Controller for retrieving document type JSON schemas.
/// </summary>
[ApiVersion("1.0")]
public class SchemaDocumentTypeController : DocumentTypeControllerBase
{
private readonly IContentTypeJsonSchemaService _schemaService;

/// <summary>
/// Initializes a new instance of the <see cref="SchemaDocumentTypeController"/> class.
/// </summary>
public SchemaDocumentTypeController(IContentTypeJsonSchemaService schemaService)
=> _schemaService = schemaService;

/// <summary>
/// Gets a JSON Schema for a specific document type.
/// </summary>
/// <param name="id">The unique identifier of the document type.</param>
/// <returns>A JSON Schema describing the document creation/update payload structure.</returns>
/// <remarks>
/// The returned JSON Schema references data type schemas via external <c>$ref</c> URIs.
/// Tooling should resolve these references by making HTTP requests to the data type schema endpoints.
/// </remarks>
[HttpGet("{id:guid}/schema")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(JsonObject), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetSchema(Guid id)
{
JsonObject? schema = await _schemaService.GetDocumentTypeSchemaAsync(id);
return schema is not null
? Ok(schema)
: OperationStatusResult(ContentTypeOperationStatus.NotFound);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Controller for retrieving media type JSON schemas.
/// </summary>
[ApiVersion("1.0")]
public class SchemaMediaTypeController : MediaTypeControllerBase
{
private readonly IContentTypeJsonSchemaService _schemaService;

/// <summary>
/// Initializes a new instance of the <see cref="SchemaMediaTypeController"/> class.
/// </summary>
public SchemaMediaTypeController(IContentTypeJsonSchemaService schemaService)
=> _schemaService = schemaService;

/// <summary>
/// Gets a JSON Schema for a specific media type.
/// </summary>
/// <param name="id">The unique identifier of the media type.</param>
/// <returns>A JSON Schema describing the media creation/update payload structure.</returns>
/// <remarks>
/// The returned JSON Schema references data type schemas via external <c>$ref</c> URIs.
/// Tooling should resolve these references by making HTTP requests to the data type schema endpoints.
/// </remarks>
[HttpGet("{id:guid}/schema")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(JsonObject), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetSchema(Guid id)
{
JsonObject? schema = await _schemaService.GetMediaTypeSchemaAsync(id);
return schema is not null
? Ok(schema)
: OperationStatusResult(ContentTypeOperationStatus.NotFound);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Controller for retrieving member type JSON schemas.
/// </summary>
[ApiVersion("1.0")]
public class SchemaMemberTypeController : MemberTypeControllerBase
{
private readonly IContentTypeJsonSchemaService _schemaService;

/// <summary>
/// Initializes a new instance of the <see cref="SchemaMemberTypeController"/> class.
/// </summary>
public SchemaMemberTypeController(IContentTypeJsonSchemaService schemaService)
=> _schemaService = schemaService;

/// <summary>
/// Gets a JSON Schema for a specific member type.
/// </summary>
/// <param name="id">The unique identifier of the member type.</param>
/// <returns>A JSON Schema describing the member creation/update payload structure.</returns>
/// <remarks>
/// The returned JSON Schema references data type schemas via external <c>$ref</c> URIs.
/// Tooling should resolve these references by making HTTP requests to the data type schema endpoints.
/// </remarks>
[HttpGet("{id:guid}/schema")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(JsonObject), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetSchema(Guid id)
{
JsonObject? schema = await _schemaService.GetMemberTypeSchemaAsync(id);
return schema is not null
? Ok(schema)
: OperationStatusResult(ContentTypeOperationStatus.NotFound);
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -11,6 +12,7 @@ internal static class DocumentTypeBuilderExtensions
internal static IUmbracoBuilder AddDocumentTypes(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IDocumentTypeEditingPresentationFactory, DocumentTypeEditingPresentationFactory>();
builder.Services.AddTransient<IContentTypeJsonSchemaService, ContentTypeJsonSchemaService>();

builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<DocumentTypeMapDefinition>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<DocumentTypeCompositionMapDefinition>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public static IUmbracoBuilder AddUmbracoManagementApi(this IUmbracoBuilder build
IServiceCollection services = builder.Services;
builder.Services.AddSingleton<BackOfficeAreaRoutes>();
builder.Services.AddSingleton<BackOfficeExternalLoginProviderErrorMiddleware>();
builder.Services.AddSingleton<IManagementApiRouteBuilder, ManagementApiRouteBuilder>();
builder.Services.AddUnique<IConflictingRouteService, ConflictingRouteService>();
builder.AddUmbracoApiOpenApiUI();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Linq.Expressions;

namespace Umbraco.Cms.Api.Management.Routing;

/// <summary>
/// Provides URL generation for Management API controllers from non-controller contexts.
/// </summary>
public interface IManagementApiRouteBuilder
{
/// <summary>
/// Generates a path to an action on a Management API controller.
/// </summary>
/// <typeparam name="TController">The controller type.</typeparam>
/// <param name="action">Expression selecting the action name (e.g., c => nameof(c.Schema)).</param>
/// <param name="routeValues">Route values (e.g., new { id = guid }).</param>
/// <returns>The generated path, or null if the route could not be found.</returns>
string? GetPathByAction<TController>(
Expression<Func<TController, string>> action,
object? routeValues = null);
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides URL generation for Management API controllers using LinkGenerator.
/// </summary>
/// <remarks>
/// This service mirrors the CreatedAtId pattern used in ManagementApiControllerBase
/// but works in non-controller contexts where IUrlHelper is not available.
/// </remarks>
internal sealed class ManagementApiRouteBuilder : IManagementApiRouteBuilder
{
private readonly LinkGenerator _linkGenerator;
private readonly ApiVersioningOptions _apiVersioningOptions;

public ManagementApiRouteBuilder(
LinkGenerator linkGenerator,
IOptions<ApiVersioningOptions> apiVersioningOptions)
{
_linkGenerator = linkGenerator;
_apiVersioningOptions = apiVersioningOptions.Value;
}

/// <inheritdoc />
public string? GetPathByAction<TController>(
Expression<Func<TController, string>> 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);
}
}
Loading
Loading