Skip to content

feat: handle union types and update patched cdk schema#1172

Draft
sai-ray wants to merge 2 commits intomainfrom
sai/schema-update-union-types
Draft

feat: handle union types and update patched cdk schema#1172
sai-ray wants to merge 2 commits intomainfrom
sai/schema-update-union-types

Conversation

@sai-ray
Copy link
Copy Markdown
Contributor

@sai-ray sai-ray commented Mar 4, 2026

Fixes #

The CloudFormation schema files (cdk-resources.json and cdk-types.json) were outdated, causing cdk-from-cfn to reject valid newer CloudFormation properties. For example,AvailabilityZoneRebalancing on AWS::ECS::Service would fail with:

AvailabilityZoneRebalancing is not a valid property for resource ECSService of type AWS::ECS::Service

This blocked cdk migrate and direct cdk-from-cfn usage for any template using properties added after the schema files were last generated.

Additionally, the updated schema introduces TypeReference::Union types that the existing code did not handle. Union types arise from multiple sources:

  • CloudFormation's Registry Schema uses JSON Schema oneOf/anyOf to express properties that accept multiple value shapes. For example, a property might accept either a structured object or a raw JSON string. The aws-service-spec importer (import-cloudformation-registry.ts) converts these oneOf/anyOf constructs directly into union types via maybeUnion().
  • aws-service-spec performs "schema unification" when merging type definitions from different sources (CloudFormation Resource Spec, Registry Schema, SAM specs). When different sources describe the same property with different types, it computes a "typed union" widening the type to accept both shapes rather than picking one and losing information.
  • cdk-patched-schema-generator creates additional unions when a property has historical type changes. Its TypeDecider.getType() checks if previousTypes exist for a property and wraps [currentType, ...previousTypes] into a union, ensuring cdk-from-cfn can handle templates written against any version of the type definition.

CloudFormation itself has no union concept. Each property has a single type. The unions are entirely a schema-level artifact. Without union support, any resource with union-typed properties would error with "is not implemented for ResourceValue::Object" during IR translation.

Changes

1. Regenerated schema files

Updated both schema files using cdklabs/cdk-patched-schema-generator, which pulls from @aws-cdk/aws-service-spec (the authoritative source combining CloudFormation Resource Specification, Registry Schema, SAM specs, and patches).

  • cdk-resources.json: 1,261 resource definitions → 1,562 resource definitions (+301 new resources added, 4 deprecated resources removed)
  • cdk-types.json: 5,896 property type definitions → 8,588 property type definitions (+2,776 new types added, 84 stale types removed)

The 4 removed resources were deprecated AWS services (AWS::DataSync::StorageSystem, 3x AWS::IoT1Click::*). The 84 removed types break down as: 4 belonged to those deprecated resources, 75 were misplaced under wrong parent resources and now exist under their correct parents, and 5 were phantom entries for a resource that never existed (AWS::CodeTest::PersistentConfiguration).

2. Added union type resolution in src/ir/resources/mod.rs

Object values

The fix resolves unions to a concrete type using a priority order: Named > Map > Json. This order reflects how much type information each variant provides for code generation. Named types are specific CDK structs with known property names and typed children, producing the richest output. Map types know the value type but not the key names. Json is fully opaque, passing everything through as-is. We always pick the most informative type available so the generated CDK code is as strongly typed as possible. Only these three can represent an object value. The resolved type is stored in the resulting ResourceIr::Object so that downstream synthesizers emit property names correctly (Named types go through camelCase conversion, while Json preserves names as-is).

For multi-Named unions (14 exist in the current schema), resolution uses two strategies:

  1. Discriminator matching for SAM event sources: SAM event objects have a Type field (e.g. "Api", "S3", "SQS") that identifies which event type they are. The resolver matches this value against Named variant names (e.g. "SQS" matches SQSEvent). This handles the AWS::Serverless::Function event union (14 Named types) and the AWS::Serverless::StateMachine event union (4 Named types) independently.
  2. Property key scoring for everything else: counts how many of the object's keys exist as properties in each Named type's schema, and picks the highest match. This handles cases like ContainerProperties vs MultiNodeContainerProperties where there's no discriminator field.

The union resolution logic is extracted into ResourceTranslator::resolve_union_for_object with detailed documentation explaining the sources of union types and the resolution strategy.

For multi-Map unions (3 exist in the current schema, all historical type pairs), the resolver picks the Map variant with the widest value type, preferring a Map whose value is a union over one with a single primitive. This ensures valid values aren't rejected when the current type accepts more shapes than the previous type (e.g. mapOf(union(boolean, string)) over mapOf(boolean)).

Array values

The Array arm also needed union handling. Properties like AWS::CloudWatch::InsightRule.Tags have type unionOf: [json, listOf(CfnTag)]. When the template value is an array, the code now extracts the List variant's item type from the union so that each array element gets properly typed translation (e.g. elements are translated through CfnTag's property bag instead of falling back to untyped JSON).

Examples

Object with primitive-only union — AWS::IAM::Role.AssumeRolePolicyDocument

Schema type: unionOf: [unionOf: [json, string], json]

AssumeRolePolicyDocument:
  Version: "2012-10-17"
  Statement:
    - Effect: Allow
      Principal:
        Service: lambda.amazonaws.com
      Action: sts:AssumeRole

Parser produces ResourceValue::Objectself.value_type is Union([Union([Json, String]), Json])resolve_union_for_object → zero Named variants → priority chain finds Primitive(Json) → property bag is MapOf(Json)Version and Statement pass through as-is → synthesizer sees Primitive structure → emits names verbatim.

Object with multi-Named union + discriminator — AWS::Serverless::Function events

Schema type: union of 14 Named types (S3Event, SNSEvent, SQSEvent, ApiEvent, ...)

Events:
  ProcessQueue:
    Type: SQS
    Properties:
      Queue: !GetAtt MyQueue.Arn
      BatchSize: 10

Parser produces ResourceValue::Object for ProcessQueueself.value_type is Union(14 Named)resolve_union_for_object → 14 Named variants → discriminator: object has "Type": "SQS" → loop variants → SQSEvent contains "SQS" → match → return Named("...SQSEvent") → property bag looks up SQSEventPropertyQueue typed as string, BatchSize as number → correctly typed CDK code.

Object with multi-Named union + property scoring — AWS::Batch::JobDefinition container

Schema type: unionOf: [Named(MultiNodeContainerProperties), Named(ContainerProperties)]

Container:
  Image: public.ecr.aws/nginx/nginx
  ResourceRequirements:
    - Type: VCPU
      Value: "1"
  NetworkConfiguration:
    AssignPublicIp: ENABLED

Parser produces ResourceValue::Object → 2 Named variants → discriminator: no "Type" key → property scoring: MultiNodeContainerProperties matches Image + ResourceRequirements (2), ContainerProperties matches Image + ResourceRequirements + NetworkConfiguration (3) → pick ContainerProperties (3 > 2) → children get correctly typed translation.

Array with union type — AWS::CloudWatch::InsightRule.Tags

Schema type: unionOf: [json, listOf(CfnTag)]

Tags:
  - Key: Environment
    Value: Production
  - Key: Team
    Value: Platform

Parser produces ResourceValue::Arrayself.value_type is Union([Json, List(CfnTag)]) → Array arm finds List(CfnTag) variant → extracts item type Named(CfnTag) → each element translated through CfnTag property bag → Key and Value typed as strings → correctly typed CDK code instead of untyped JSON passthrough.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 4, 2026

Codecov Report

❌ Patch coverage is 52.94118% with 40 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.2%. Comparing base (1ab32b6) to head (ca00b6f).

Files with missing lines Patch % Lines
src/ir/resources/mod.rs 52.9% 40 Missing ⚠️
Additional details and impacted files
Components Coverage Δ
Parser 86.1% <ø> (ø)
Intermediate Representation 88.5% <52.9%> (-3.3%) ⬇️
Synthesizers 96.2% <ø> (ø)
Other 96.4% <ø> (ø)
@@           Coverage Diff           @@
##            main   #1172     +/-   ##
=======================================
- Coverage   92.6%   92.2%   -0.4%     
=======================================
  Files         25      25             
  Lines       5559    5641     +82     
  Branches    5559    5641     +82     
=======================================
+ Hits        5150    5201     +51     
- Misses       409     440     +31     
Files with missing lines Coverage Δ
src/ir/resources/mod.rs 86.3% <52.9%> (-5.2%) ⬇️

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1ab32b6...ca00b6f. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

1 participant