Skip to content

Add inline fields support on union type entries#961

Closed
mbryzek wants to merge 4 commits intomainfrom
type-safe-union-fields
Closed

Add inline fields support on union type entries#961
mbryzek wants to merge 4 commits intomainfrom
type-safe-union-fields

Conversation

@mbryzek
Copy link
Collaborator

@mbryzek mbryzek commented Feb 26, 2026

Summary

  • Union type entries now support an optional fields property
  • API Builder will also auto create Models for any union types that are otherwise not found (syntactic sugar)
  • When fields is present, the type will support the specified fields
  • When fields is absent, existing behavior is preserved
  • This change is 100% backward compatible

Example

{
  "unions": {
    "task_type": {
      "discriminator": "discriminator",
      "types": [
        { "type": "game" },
        { "type": "merge_person", "fields": [
          { "name": "user_id", "type": "string" },
          { "name": "person_id", "type": "string" }
        ]}
      ]
    }
  }
}

Union type entries now support an optional `fields` property. When
present, a model is auto-generated using the type name with the
specified fields. Use an empty array for types that carry no data.
This provides a compact way to define union member types inline
without separate model declarations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 26, 2026 20:10
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for defining union type members inline with their fields, providing a more concise way to declare union types without requiring separate model declarations. When a union type entry includes a fields property, API Builder automatically generates a model using the type name with those fields.

Changes:

  • Added optional fields property to union type entries in both API spec files
  • Implemented synthetic model generation from union types with inline fields
  • Added comprehensive test coverage with 9 new tests covering various scenarios
  • Updated documentation to describe the new inline fields feature

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
spec/apibuilder-spec.json Added optional fields property definition to union_type model
spec/apibuilder-api-json.json Added optional fields property definition to union_type in API JSON spec
core/test/core/UnionTypeInlineFieldsSpec.scala New test file with 9 comprehensive tests covering inline fields functionality
core/app/builder/api_json/InternalApiJsonForm.scala Implemented synthetic model generation from union type inline fields
app/app/views/doc/apiJson.scala.html Updated documentation to describe the new inline fields feature

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

mbryzek and others added 3 commits February 26, 2026 15:32
@mbryzek mbryzek requested a review from Copilot February 27, 2026 23:07
@mbryzek mbryzek closed this Feb 27, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 10 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

default = JsonUtil.asOptBoolean(json \ "default"),
discriminatorValue = JsonUtil.asOptString(json \ "discriminator_value"),
fields = fields,
attributes = InternalAttributeForm.fromJson((value \ "attributes").asOpt[JsArray]),
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

InternalUnionTypeForm.attributes is being parsed from the parent union object (value \ "attributes") instead of the union type entry object (json \ "attributes"). This will incorrectly apply union-level attributes to every member type and ignore per-type attributes. Parse attributes from the union type JSON object.

Suggested change
attributes = InternalAttributeForm.fromJson((value \ "attributes").asOpt[JsArray]),
attributes = InternalAttributeForm.fromJson((json \ "attributes").asOpt[JsArray]),

Copilot uses AI. Check for mistakes.
Comment on lines +453 to +467
val fields: Option[Seq[InternalFieldForm]] =
(json \ "fields").asOpt[JsArray].map { _ =>
InternalFieldForm.parse(internalDatatypeBuilder, json)
}

fields.filter(_.nonEmpty).foreach { fieldForms =>
datatypeName.foreach { name =>
internalDatatypeBuilder.addDynamicModel(
InternalModelForm(
name = name,
plural = Text.pluralize(name),
description = JsonUtil.asOptString(json \ "description"),
deprecation = InternalDeprecationForm.fromJsValue(json),
fields = fieldForms,
attributes = Nil,
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

With the new fields support, the code will currently attempt to auto-generate models for any type value (including primitives like string or container types like [guest] / map[foo]) when fields is present. This can create invalid or conflicting models (e.g., model named string) while the union member still resolves as a primitive/list. Add validation that fields can only be used with a plain, non-primitive singleton type name (i.e., not a primitive, list, or map), and surface a clear error when violated.

Copilot uses AI. Check for mistakes.
Comment on lines +458 to +474
fields.filter(_.nonEmpty).foreach { fieldForms =>
datatypeName.foreach { name =>
internalDatatypeBuilder.addDynamicModel(
InternalModelForm(
name = name,
plural = Text.pluralize(name),
description = JsonUtil.asOptString(json \ "description"),
deprecation = InternalDeprecationForm.fromJsValue(json),
fields = fieldForms,
attributes = Nil,
interfaces = Nil,
templates = Nil,
warnings = ().validNec
)
)
}
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

Inline union member models are only synthesized when the parsed fields list is non-empty (fields.filter(_.nonEmpty)), which contradicts the new spec/docs/tests that say fields: [] should generate an empty model for no-data types. Create the dynamic model whenever fields is present (even if empty), not only when it’s non-empty.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants