Skip to content

Commit 367d76f

Browse files
committed
feat: migrate to Pydantic v2
BREAKING CHANGE: Requires pydantic>=2.0 - Update pydantic dependency from v1 to v2 - Remove v1/v2 compatibility shim in samtranslator/compat.py - Migrate deprecated APIs: - parse_obj() -> model_validate() - schema() -> model_json_schema() - dict() -> model_dump() - __root__ -> RootModel with .root accessor - class Config -> model_config = ConfigDict() - Update Field() to use json_schema_extra for custom schema properties - Add explicit default values for all Optional fields - Add type aliases to avoid field name shadowing in Pydantic v2 - Update JSON schema generation to normalize $defs -> definitions - Upgrade schema version from draft-04 to draft-07 - Update validation error handling for v2 error format - Update mypy to >=1.5.0 for Pydantic v2 plugin compatibility - Add hypothesis test dependency
1 parent a45a711 commit 367d76f

35 files changed

Lines changed: 13118 additions & 5527 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,4 @@ venv.bak/
120120
integration/config/file_to_s3_map_modified.json
121121

122122
.tmp
123+
.kiro

requirements/base.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ jsonschema<5,>=3.2 # TODO: evaluate risk of removing jsonschema 3.x support
33
typing_extensions>=4.4 # 3.8 doesn't have Required, TypeGuard and ParamSpec
44

55
# resource validation & schema generation
6-
# 1.10.15 and 1.10.17 included breaking change from pydantic, more info: https://github.com/aws/serverless-application-model/issues/3617
7-
pydantic>=1.8,<3,!=1.10.15,!=1.10.17
6+
pydantic>=2.0,<3

requirements/dev.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ruff~=0.4.5
99
# Test requirements
1010
pytest>=6.2,<8
1111
parameterized~=0.7
12+
hypothesis>=6.0,<7
1213

1314
# Integration tests
1415
dateparser~=1.1
@@ -23,7 +24,7 @@ black==24.3.0
2324
ruamel.yaml==0.17.21 # It can parse yaml while perserving comments
2425

2526
# type check
26-
mypy~=1.3.0
27+
mypy>=1.5.0,<2.0
2728

2829
# types
2930
boto3-stubs[appconfig,serverlessrepo]>=1.34.0,<2.0.0

samtranslator/compat.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,4 @@
1-
try:
2-
from pydantic import v1 as pydantic
3-
4-
# Starting Pydantic v1.10.17, pydantic import v1 will success,
5-
# adding the following line to make Pydantic v1 should fall back to v1 import correctly.
6-
pydantic.error_wrappers.ValidationError # noqa
7-
except ImportError:
8-
# Unfortunately mypy cannot handle this try/expect pattern, and "type: ignore"
9-
# is the simplest work-around. See: https://github.com/python/mypy/issues/1153
10-
import pydantic # type: ignore
11-
except AttributeError:
12-
# Pydantic v1.10.17+
13-
import pydantic # type: ignore
1+
# Pydantic v2 direct import - no compatibility shim needed
2+
import pydantic
143

154
__all__ = ["pydantic"]
Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1-
from samtranslator.compat import pydantic
2-
from samtranslator.internal.schema_source.common import LenientBaseModel
1+
from typing import Any, Dict
2+
3+
from pydantic import field_validator
34

4-
constr = pydantic.constr
5+
from samtranslator.internal.schema_source.common import LenientBaseModel
56

67

78
# Anything goes if has string Type but is not AWS::Serverless::*
89
class Resource(LenientBaseModel):
9-
Type: constr(regex=r"^(?!AWS::Serverless::).+$") # type: ignore
10+
Type: str
11+
12+
# Use model_json_schema_extra to add the pattern to JSON Schema
13+
# Pydantic's Rust regex doesn't support lookahead, but JSON Schema validators do
14+
model_config = {
15+
"json_schema_extra": lambda schema, _: _add_type_pattern(schema),
16+
}
17+
18+
@field_validator("Type")
19+
@classmethod
20+
def type_must_not_be_serverless(cls, v: str) -> str:
21+
"""Validate that Type does not start with AWS::Serverless::"""
22+
if v.startswith("AWS::Serverless::"):
23+
raise ValueError("Type must not start with 'AWS::Serverless::'")
24+
return v
25+
26+
27+
def _add_type_pattern(schema: Dict[str, Any]) -> None:
28+
"""Add pattern constraint to Type field in JSON Schema."""
29+
if "properties" in schema and "Type" in schema["properties"]:
30+
schema["properties"]["Type"]["pattern"] = r"^(?!AWS::Serverless::).+$"

samtranslator/internal/schema_source/aws_serverless_api.py

Lines changed: 87 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,14 @@ class UsagePlan(BaseModel):
101101
UsagePlanName: Optional[PassThroughProp] = usageplan("UsagePlanName")
102102

103103

104+
# Type aliases to avoid field name shadowing class names
105+
_ResourcePolicy = ResourcePolicy
106+
_UsagePlan = UsagePlan
107+
108+
104109
class Auth(BaseModel):
105110
AddDefaultAuthorizerToCorsPreflight: Optional[bool] = auth("AddDefaultAuthorizerToCorsPreflight")
106-
AddApiKeyRequiredToCorsPreflight: Optional[bool] # TODO Add Docs
111+
AddApiKeyRequiredToCorsPreflight: Optional[bool] = None # TODO Add Docs
107112
ApiKeyRequired: Optional[bool] = auth("ApiKeyRequired")
108113
Authorizers: Optional[
109114
Dict[
@@ -117,8 +122,12 @@ class Auth(BaseModel):
117122
] = auth("Authorizers")
118123
DefaultAuthorizer: Optional[str] = auth("DefaultAuthorizer")
119124
InvokeRole: Optional[str] = auth("InvokeRole")
120-
ResourcePolicy: Optional[ResourcePolicy] = auth("ResourcePolicy")
121-
UsagePlan: Optional[UsagePlan] = auth("UsagePlan")
125+
ResourcePolicy: Optional[_ResourcePolicy] = auth("ResourcePolicy")
126+
UsagePlan: Optional[_UsagePlan] = auth("UsagePlan")
127+
128+
129+
# Type alias to avoid field name shadowing class name
130+
_Auth = Auth
122131

123132

124133
class Cors(BaseModel):
@@ -151,21 +160,26 @@ class Route53(BaseModel):
151160
["AWS::Route53::RecordSetGroup.RecordSet", "HostedZoneName"],
152161
)
153162
IpV6: Optional[bool] = route53("IpV6")
154-
SetIdentifier: Optional[PassThroughProp] # TODO: add docs
155-
Region: Optional[PassThroughProp] # TODO: add docs
156-
SeparateRecordSetGroup: Optional[bool] # TODO: add docs
157-
VpcEndpointDomainName: Optional[PassThroughProp] # TODO: add docs
158-
VpcEndpointHostedZoneId: Optional[PassThroughProp] # TODO: add docs
163+
SetIdentifier: Optional[PassThroughProp] = None # TODO: add docs
164+
Region: Optional[PassThroughProp] = None # TODO: add docs
165+
SeparateRecordSetGroup: Optional[bool] = None # TODO: add docs
166+
VpcEndpointDomainName: Optional[PassThroughProp] = None # TODO: add docs
167+
VpcEndpointHostedZoneId: Optional[PassThroughProp] = None # TODO: add docs
159168

160169

161170
class AccessAssociation(BaseModel):
162171
VpcEndpointId: PassThroughProp # TODO: add docs
163172

164173

174+
# Type aliases to avoid field name shadowing class names
175+
_Route53 = Route53
176+
_AccessAssociation = AccessAssociation
177+
178+
165179
class Domain(BaseModel):
166180
BasePath: Optional[PassThroughProp] = domain("BasePath")
167181
NormalizeBasePath: Optional[bool] = domain("NormalizeBasePath")
168-
Policy: Optional[PassThroughProp]
182+
Policy: Optional[PassThroughProp] = None
169183
CertificateArn: PassThroughProp = domain("CertificateArn")
170184
DomainName: PassThroughProp = passthrough_prop(
171185
DOMAIN_STEM,
@@ -190,13 +204,17 @@ class Domain(BaseModel):
190204
"OwnershipVerificationCertificateArn",
191205
["AWS::ApiGateway::DomainName", "Properties", "OwnershipVerificationCertificateArn"],
192206
)
193-
Route53: Optional[Route53] = domain("Route53")
207+
Route53: Optional[_Route53] = domain("Route53")
194208
SecurityPolicy: Optional[PassThroughProp] = passthrough_prop(
195209
DOMAIN_STEM,
196210
"SecurityPolicy",
197211
["AWS::ApiGateway::DomainName", "Properties", "SecurityPolicy"],
198212
)
199-
AccessAssociation: Optional[AccessAssociation]
213+
AccessAssociation: Optional[_AccessAssociation] = None
214+
215+
216+
# Type alias to avoid field name shadowing class name
217+
_Domain = Domain
200218

201219

202220
class DefinitionUri(BaseModel):
@@ -235,27 +253,28 @@ class EndpointConfiguration(BaseModel):
235253
)
236254

237255

238-
Name = Optional[PassThroughProp]
239-
DefinitionUriType = Optional[Union[str, DefinitionUri]]
240-
MergeDefinitions = Optional[bool]
241-
CacheClusterEnabled = Optional[PassThroughProp]
242-
CacheClusterSize = Optional[PassThroughProp]
243-
Variables = Optional[PassThroughProp]
244-
EndpointConfigurationType = Optional[SamIntrinsicable[EndpointConfiguration]]
245-
MethodSettings = Optional[PassThroughProp]
246-
BinaryMediaTypes = Optional[PassThroughProp]
247-
MinimumCompressionSize = Optional[PassThroughProp]
248-
CorsType = Optional[SamIntrinsicable[Union[str, Cors]]]
249-
GatewayResponses = Optional[DictStrAny]
250-
AccessLogSetting = Optional[PassThroughProp]
251-
CanarySetting = Optional[PassThroughProp]
252-
TracingEnabled = Optional[PassThroughProp]
253-
OpenApiVersion = Optional[Union[float, str]] # TODO: float doesn't exist in documentation
254-
AlwaysDeploy = Optional[bool]
256+
# Type aliases with underscore prefix to avoid shadowing by field names
257+
_Name = Optional[PassThroughProp]
258+
_DefinitionUriType = Optional[Union[str, DefinitionUri]]
259+
_MergeDefinitions = Optional[bool]
260+
_CacheClusterEnabled = Optional[PassThroughProp]
261+
_CacheClusterSize = Optional[PassThroughProp]
262+
_Variables = Optional[PassThroughProp]
263+
_EndpointConfigurationType = Optional[SamIntrinsicable[EndpointConfiguration]]
264+
_MethodSettings = Optional[PassThroughProp]
265+
_BinaryMediaTypes = Optional[PassThroughProp]
266+
_MinimumCompressionSize = Optional[PassThroughProp]
267+
_CorsType = Optional[SamIntrinsicable[Union[str, Cors]]]
268+
_GatewayResponses = Optional[DictStrAny]
269+
_AccessLogSetting = Optional[PassThroughProp]
270+
_CanarySetting = Optional[PassThroughProp]
271+
_TracingEnabled = Optional[PassThroughProp]
272+
_OpenApiVersion = Optional[Union[float, str]] # TODO: float doesn't exist in documentation
273+
_AlwaysDeploy = Optional[bool]
255274

256275

257276
class Properties(BaseModel):
258-
AccessLogSetting: Optional[AccessLogSetting] = passthrough_prop(
277+
AccessLogSetting: Optional[_AccessLogSetting] = passthrough_prop(
259278
PROPERTIES_STEM,
260279
"AccessLogSetting",
261280
["AWS::ApiGateway::Stage", "Properties", "AccessLogSetting"],
@@ -265,47 +284,47 @@ class Properties(BaseModel):
265284
"ApiKeySourceType",
266285
["AWS::ApiGateway::RestApi", "Properties", "ApiKeySourceType"],
267286
)
268-
Auth: Optional[Auth] = properties("Auth")
269-
BinaryMediaTypes: Optional[BinaryMediaTypes] = properties("BinaryMediaTypes")
270-
CacheClusterEnabled: Optional[CacheClusterEnabled] = passthrough_prop(
287+
Auth: Optional[_Auth] = properties("Auth")
288+
BinaryMediaTypes: Optional[_BinaryMediaTypes] = properties("BinaryMediaTypes")
289+
CacheClusterEnabled: Optional[_CacheClusterEnabled] = passthrough_prop(
271290
PROPERTIES_STEM,
272291
"CacheClusterEnabled",
273292
["AWS::ApiGateway::Stage", "Properties", "CacheClusterEnabled"],
274293
)
275-
CacheClusterSize: Optional[CacheClusterSize] = passthrough_prop(
294+
CacheClusterSize: Optional[_CacheClusterSize] = passthrough_prop(
276295
PROPERTIES_STEM,
277296
"CacheClusterSize",
278297
["AWS::ApiGateway::Stage", "Properties", "CacheClusterSize"],
279298
)
280-
CanarySetting: Optional[CanarySetting] = passthrough_prop(
299+
CanarySetting: Optional[_CanarySetting] = passthrough_prop(
281300
PROPERTIES_STEM,
282301
"CanarySetting",
283302
["AWS::ApiGateway::Stage", "Properties", "CanarySetting"],
284303
)
285-
Cors: Optional[CorsType] = properties("Cors")
304+
Cors: Optional[_CorsType] = properties("Cors")
286305
DefinitionBody: Optional[DictStrAny] = properties("DefinitionBody")
287-
DefinitionUri: Optional[DefinitionUriType] = properties("DefinitionUri")
288-
MergeDefinitions: Optional[MergeDefinitions] = properties("MergeDefinitions")
306+
DefinitionUri: Optional[_DefinitionUriType] = properties("DefinitionUri")
307+
MergeDefinitions: Optional[_MergeDefinitions] = properties("MergeDefinitions")
289308
Description: Optional[PassThroughProp] = passthrough_prop(
290309
PROPERTIES_STEM,
291310
"Description",
292311
["AWS::ApiGateway::Stage", "Properties", "Description"],
293312
)
294313
DisableExecuteApiEndpoint: Optional[PassThroughProp] = properties("DisableExecuteApiEndpoint")
295-
Domain: Optional[Domain] = properties("Domain")
296-
EndpointConfiguration: Optional[EndpointConfigurationType] = properties("EndpointConfiguration")
314+
Domain: Optional[_Domain] = properties("Domain")
315+
EndpointConfiguration: Optional[_EndpointConfigurationType] = properties("EndpointConfiguration")
297316
FailOnWarnings: Optional[PassThroughProp] = passthrough_prop(
298317
PROPERTIES_STEM,
299318
"FailOnWarnings",
300319
["AWS::ApiGateway::RestApi", "Properties", "FailOnWarnings"],
301320
)
302-
GatewayResponses: Optional[GatewayResponses] = properties("GatewayResponses")
303-
MethodSettings: Optional[MethodSettings] = passthrough_prop(
321+
GatewayResponses: Optional[_GatewayResponses] = properties("GatewayResponses")
322+
MethodSettings: Optional[_MethodSettings] = passthrough_prop(
304323
PROPERTIES_STEM,
305324
"MethodSettings",
306325
["AWS::ApiGateway::Stage", "Properties", "MethodSettings"],
307326
)
308-
MinimumCompressionSize: Optional[MinimumCompressionSize] = passthrough_prop(
327+
MinimumCompressionSize: Optional[_MinimumCompressionSize] = passthrough_prop(
309328
PROPERTIES_STEM,
310329
"MinimumCompressionSize",
311330
["AWS::ApiGateway::RestApi", "Properties", "MinimumCompressionSize"],
@@ -316,85 +335,85 @@ class Properties(BaseModel):
316335
["AWS::ApiGateway::RestApi", "Properties", "Mode"],
317336
)
318337
Models: Optional[DictStrAny] = properties("Models")
319-
Name: Optional[Name] = passthrough_prop(
338+
Name: Optional[_Name] = passthrough_prop(
320339
PROPERTIES_STEM,
321340
"Name",
322341
["AWS::ApiGateway::RestApi", "Properties", "Name"],
323342
)
324-
OpenApiVersion: Optional[OpenApiVersion] = properties("OpenApiVersion")
343+
OpenApiVersion: Optional[_OpenApiVersion] = properties("OpenApiVersion")
325344
StageName: SamIntrinsicable[str] = properties("StageName")
326345
Tags: Optional[DictStrAny] = properties("Tags")
327-
Policy: Optional[PassThroughProp] # TODO: add docs
328-
PropagateTags: Optional[bool] # TODO: add docs
329-
TracingEnabled: Optional[TracingEnabled] = passthrough_prop(
346+
Policy: Optional[PassThroughProp] = None # TODO: add docs
347+
PropagateTags: Optional[bool] = None # TODO: add docs
348+
TracingEnabled: Optional[_TracingEnabled] = passthrough_prop(
330349
PROPERTIES_STEM,
331350
"TracingEnabled",
332351
["AWS::ApiGateway::Stage", "Properties", "TracingEnabled"],
333352
)
334-
Variables: Optional[Variables] = passthrough_prop(
353+
Variables: Optional[_Variables] = passthrough_prop(
335354
PROPERTIES_STEM,
336355
"Variables",
337356
["AWS::ApiGateway::Stage", "Properties", "Variables"],
338357
)
339-
AlwaysDeploy: Optional[AlwaysDeploy] = properties("AlwaysDeploy")
358+
AlwaysDeploy: Optional[_AlwaysDeploy] = properties("AlwaysDeploy")
340359

341360

342361
class Globals(BaseModel):
343-
Auth: Optional[Auth] = properties("Auth")
344-
Name: Optional[Name] = passthrough_prop(
362+
Auth: Optional[_Auth] = properties("Auth")
363+
Name: Optional[_Name] = passthrough_prop(
345364
PROPERTIES_STEM,
346365
"Name",
347366
["AWS::ApiGateway::RestApi", "Properties", "Name"],
348367
)
349368
DefinitionUri: Optional[PassThroughProp] = properties("DefinitionUri")
350-
CacheClusterEnabled: Optional[CacheClusterEnabled] = passthrough_prop(
369+
CacheClusterEnabled: Optional[_CacheClusterEnabled] = passthrough_prop(
351370
PROPERTIES_STEM,
352371
"CacheClusterEnabled",
353372
["AWS::ApiGateway::Stage", "Properties", "CacheClusterEnabled"],
354373
)
355-
CacheClusterSize: Optional[CacheClusterSize] = passthrough_prop(
374+
CacheClusterSize: Optional[_CacheClusterSize] = passthrough_prop(
356375
PROPERTIES_STEM,
357376
"CacheClusterSize",
358377
["AWS::ApiGateway::Stage", "Properties", "CacheClusterSize"],
359378
)
360-
MergeDefinitions: Optional[MergeDefinitions] = properties("MergeDefinitions")
361-
Variables: Optional[Variables] = passthrough_prop(
379+
MergeDefinitions: Optional[_MergeDefinitions] = properties("MergeDefinitions")
380+
Variables: Optional[_Variables] = passthrough_prop(
362381
PROPERTIES_STEM,
363382
"Variables",
364383
["AWS::ApiGateway::Stage", "Properties", "Variables"],
365384
)
366385
EndpointConfiguration: Optional[PassThroughProp] = properties("EndpointConfiguration")
367-
MethodSettings: Optional[MethodSettings] = properties("MethodSettings")
368-
BinaryMediaTypes: Optional[BinaryMediaTypes] = properties("BinaryMediaTypes")
369-
MinimumCompressionSize: Optional[MinimumCompressionSize] = passthrough_prop(
386+
MethodSettings: Optional[_MethodSettings] = properties("MethodSettings")
387+
BinaryMediaTypes: Optional[_BinaryMediaTypes] = properties("BinaryMediaTypes")
388+
MinimumCompressionSize: Optional[_MinimumCompressionSize] = passthrough_prop(
370389
PROPERTIES_STEM,
371390
"MinimumCompressionSize",
372391
["AWS::ApiGateway::RestApi", "Properties", "MinimumCompressionSize"],
373392
)
374-
Cors: Optional[CorsType] = properties("Cors")
375-
GatewayResponses: Optional[GatewayResponses] = properties("GatewayResponses")
376-
AccessLogSetting: Optional[AccessLogSetting] = passthrough_prop(
393+
Cors: Optional[_CorsType] = properties("Cors")
394+
GatewayResponses: Optional[_GatewayResponses] = properties("GatewayResponses")
395+
AccessLogSetting: Optional[_AccessLogSetting] = passthrough_prop(
377396
PROPERTIES_STEM,
378397
"AccessLogSetting",
379398
["AWS::ApiGateway::Stage", "Properties", "AccessLogSetting"],
380399
)
381-
CanarySetting: Optional[CanarySetting] = passthrough_prop(
400+
CanarySetting: Optional[_CanarySetting] = passthrough_prop(
382401
PROPERTIES_STEM,
383402
"CanarySetting",
384403
["AWS::ApiGateway::Stage", "Properties", "CanarySetting"],
385404
)
386-
TracingEnabled: Optional[TracingEnabled] = passthrough_prop(
405+
TracingEnabled: Optional[_TracingEnabled] = passthrough_prop(
387406
PROPERTIES_STEM,
388407
"TracingEnabled",
389408
["AWS::ApiGateway::Stage", "Properties", "TracingEnabled"],
390409
)
391-
OpenApiVersion: Optional[OpenApiVersion] = properties("OpenApiVersion")
392-
Domain: Optional[Domain] = properties("Domain")
393-
AlwaysDeploy: Optional[AlwaysDeploy] = properties("AlwaysDeploy")
394-
PropagateTags: Optional[bool] # TODO: add docs
410+
OpenApiVersion: Optional[_OpenApiVersion] = properties("OpenApiVersion")
411+
Domain: Optional[_Domain] = properties("Domain")
412+
AlwaysDeploy: Optional[_AlwaysDeploy] = properties("AlwaysDeploy")
413+
PropagateTags: Optional[bool] = None # TODO: add docs
395414

396415

397416
class Resource(ResourceAttributes):
398417
Type: Literal["AWS::Serverless::Api"]
399418
Properties: Properties
400-
Connectors: Optional[Dict[str, EmbeddedConnector]]
419+
Connectors: Optional[Dict[str, EmbeddedConnector]] = None

0 commit comments

Comments
 (0)