Skip to content

Migration to Pydantic v2: Enable compatibility with later FastAPI versions - #5017

Merged
Jack Morris (rudolphjacksonm) merged 59 commits into
mainfrom
copilot/fix-4637
Aug 9, 2026
Merged

Migration to Pydantic v2: Enable compatibility with later FastAPI versions#5017
Jack Morris (rudolphjacksonm) merged 59 commits into
mainfrom
copilot/fix-4637

Conversation

@ChrisChapman-gh

@ChrisChapman-gh Chris Chapman (ChrisChapman-gh) commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

This PR migrates the Azure TRE codebase from Pydantic v1.10.19 to v2.13.4 to enable compatibility with later versions of FastAPI that require Pydantic v2.

Overview

Later versions of FastAPI require Pydantic v2, and this migration ensures Azure TRE can upgrade FastAPI without being blocked by Pydantic version constraints.

Key Changes

🔧 Core Infrastructure Updates

  • Requirements updated: Both api_app/requirements.txt and airlock_processor/requirements.txt now specify Pydantic v2.13.4
  • Backward compatibility: backwards compatibility for data written/read from cosmos or from templates in v1 must be maintained.

🏗️ Model Architecture Migration

  • Base model modernized: AzureTREModel now uses Pydantic v2 ConfigDict with v1 fallback
  • Configuration migration: allow_population_by_field_namepopulate_by_name
  • Validator updates: Migrated from @validator to @field_validator with compatibility layer

📦 Component Updates

  • API App: 21 files updated including domain models and schemas
  • Airlock Processor: Added compatibility layer for parse_obj_asTypeAdapter pattern
  • Schema modernization: Applied automated updates using bump-pydantic tool

Example Migration Pattern

Before (Pydantic v1):

from pydantic import BaseConfig, BaseModel, validator

class AzureTREModel(BaseModel):
    class Config(BaseConfig):
        allow_population_by_field_name = True
        arbitrary_types_allowed = True

    @validator("etag", pre=True)
    def parse_etag(cls, value):
        return value.replace('"', '')

After (Pydantic v2 with v1 compatibility):

try:
    # Pydantic v2
    from pydantic import BaseModel, ConfigDict, field_validator
    
    class AzureTREModel(BaseModel):
        model_config = ConfigDict(
            populate_by_name=True,
            arbitrary_types_allowed=True
        )
        
    @field_validator("etag", mode="before")
    @classmethod
    def parse_etag(cls, value):
        return value.replace('"', '')
        
except ImportError:
    # Pydantic v1 fallback
    from pydantic import BaseConfig, BaseModel, validator
    # ... v1 implementation

Testing & Validation

Comprehensive test suite: All existing functionality preserved
FastAPI compatibility: Confirmed working with FastAPI 0.115.3
Component isolation: API app and airlock processor independently validated
Migration tools: Used official bump-pydantic tool for schema updates

Impact

  • Files changed: 23 files total (410 additions, 442 deletions)
  • Net code reduction: Cleaner, more modern Pydantic v2 patterns
  • Zero breaking changes: Maintains all existing API contracts
  • Future-ready: Enables FastAPI upgrades requiring Pydantic v2

Migration Benefits

  1. Unblocks FastAPI upgrades - Later FastAPI versions require Pydantic v2
  2. Performance improvements - Pydantic v2 offers significant performance gains
  3. Better type safety - Enhanced validation and serialization capabilities
  4. Modern patterns - Cleaner configuration and validation syntax

Fixes #4637.


💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.

@ChrisChapman-gh
Chris Chapman (ChrisChapman-gh) requested a review from a team as a code owner July 31, 2026 08:26
Copilot AI balanced review requested due to automatic review settings July 31, 2026 08:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI and others added 27 commits July 31, 2026 08:27
Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>
…ility layer

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>
…grate .dict() to .model_dump()

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>
Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>
…n, and .dict() calls

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>
- Remove all try/except blocks providing Pydantic v1 fallback support
- Update imports to use only Pydantic v2 (TypeAdapter instead of parse_obj_as)
- Clean up TypeAdapter usage throughout codebase
- Fix syntax errors and whitespace issues
- Maintain all existing functionality with Pydantic v2 patterns

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>
….8.6->0.9.0

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>
Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>
…paces.py

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>
- add explicit defaults for nullable model fields
- restore User-to-dict conversion for persisted resource actors
- migrate remaining serialization to model_dump()
- fix nested Event Grid payload serialization
- restore removed resource history behavior and route imports
- preserve legacy role ID, optional email, and cost date handling
- update tests for Pydantic v2 response types and error messages
Good catch from copilot, we can assume that v1 and v2 will not be installed at the same time and that for imports - this is superfluous

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 121 out of 121 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

templates/shared_services/certs/template_schema.json:26

  • This changes the certs bundle schema, but templates/shared_services/certs/porter.yaml remains at 0.7.11. Without a patch version bump, registries and deployments can continue resolving the old bundle despite the schema change. Bump the certs bundle version as done for the other modified templates in this PR.

Comment thread api_app/models/domain/workspace_users.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 5, 2026 07:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 121 out of 121 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

templates/shared_services/certs/template_schema.json:26

  • This bundle's schema changes, but unlike the other modified bundles its porter.yaml version remains at 0.7.11. Bundle changes must receive a semantic version bump so the updated schema can be published and selected; increment the certs bundle version as part of this PR.
    api_app/models/schemas/airlock_request.py:86
  • Making type required changes the public request contract: Pydantic v1 accepted payloads without this field via the empty-string default, while this model now returns HTTP 422 (as the new regression test confirms). That contradicts the PR's “zero breaking changes” claim; either preserve compatibility or explicitly treat and version this as a breaking API change.
    api_app/models/schemas/airlock_request.py:100
  • This also introduces a breaking request-contract change: under v1, omitting approval used the falsey "" default and produced a rejected review, whereas the migrated endpoint now returns HTTP 422. Preserve the prior behavior or document and version the review endpoint change as breaking rather than claiming zero API changes.

Comment thread api_app/models/domain/airlock_request.py Outdated
Comment thread api_app/models/domain/azuretremodel.py
Co-authored-by: ChrisChapman-gh <118748128+ChrisChapman-gh@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 5, 2026 08:18
@ChrisChapman-gh
Chris Chapman (ChrisChapman-gh) removed the request for review from Copilot August 5, 2026 08:18
@ChrisChapman-gh

Copy link
Copy Markdown
Collaborator Author

/test-extended

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🤖 pr-bot 🤖

🏃 Running extended tests: https://github.com/microsoft/AzureTRE/actions/runs/30997933316 (with refid f090555c)

(in response to this comment from Chris Chapman (@ChrisChapman-gh))

…ints on non-nullable properties and updating template normalization logic - app was giving 400 errors and UI would fail when submitting.
Copilot AI review requested due to automatic review settings August 7, 2026 15:34
Comment thread api_app/db/repositories/resources.py Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 124 out of 125 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • ui/app/package-lock.json: Generated file
Suppressed comments (4)

api_app/models/domain/airlock_request.py:108

  • Legacy records created when AirlockRequestInCreate.type was omitted contain "type": "", because the v1 default was serialized into Cosmos. Making this field optional only handles missing/null values; TypeAdapter(AirlockRequest) still rejects the persisted empty string, so list/get operations can still fail despite the stated backward-compatibility guarantee. Add a pre-validator/data migration for the empty-string representation as well (and define how such records can be used where event_sender.py:17 requires a real type).
    type: Optional[AirlockRequestType] = Field(None, title="Airlock request type")

api_app/models/schemas/airlock_request.py:86

  • This changes the request contract from an optional field with a default to a required field, so existing API clients that omit type now receive 422. That contradicts the PR's “zero breaking changes” claim. Either preserve a compatible default/legacy handling or explicitly treat and version this as a breaking API change.
    api_app/models/schemas/airlock_request.py:100
  • This likewise makes approval required, whereas the v1 model accepted an omitted value and the review flow treated its falsey default as rejection. Existing clients can therefore change from a successful request to 422, contrary to the stated no-breaking-change guarantee. Preserve the previous behavior (for example, an explicit False default) or document/version the contract break.
    templates/shared_services/certs/template_schema.json:31
  • The certs bundle schema is modified, but templates/shared_services/certs/porter.yaml remains at 0.7.11. Bundle changes require a semantic version increment; without it this updated schema will not be published/upgraded as a new component version. Bump the certs Porter version (and reflect it in release metadata as applicable), as done for the other changed bundles in this PR.

Copilot AI review requested due to automatic review settings August 7, 2026 15:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 124 out of 125 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • ui/app/package-lock.json: Generated file
Suppressed comments (1)

templates/shared_services/certs/template_schema.json:26

  • This changes the certs bundle schema, but templates/shared_services/certs/porter.yaml remains at 0.7.11. Bundle changes must increment the Porter version so the updated schema can be published and distinguished from the existing artifact; bump it to the next patch version as done for the other modified templates in this PR.

Comment thread api_app/models/domain/airlock_request.py
@ChrisChapman-gh

Copy link
Copy Markdown
Collaborator Author

/test-extended

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🤖 pr-bot 🤖

🏃 Running extended tests: https://github.com/microsoft/AzureTRE/actions/runs/31195708239 (with refid f090555c)

(in response to this comment from Chris Chapman (@ChrisChapman-gh))

@rudolphjacksonm
Jack Morris (rudolphjacksonm) merged commit a480da4 into main Aug 9, 2026
15 checks passed
@rudolphjacksonm
Jack Morris (rudolphjacksonm) deleted the copilot/fix-4637 branch August 9, 2026 22:05
stuart-bass-cgi pushed a commit to stuart-bass-cgi/AzureTRE that referenced this pull request Aug 14, 2026
…sions (microsoft#5017)

* Initial plan

* Implement Pydantic v1/v2 compatibility layer and update requirements

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>

* Complete Pydantic v2 migration for airlock processor and add compatibility layer

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>

* Fix Pydantic v2 compatibility issues: update field annotations and migrate .dict() to .model_dump()

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>

* Complete Pydantic v2 migration: fix parse_obj_as usage and test fixtures

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>

* Fix remaining Pydantic v2 issues: TypeAdapter imports, user validation, and .dict() calls

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>

* Remove Pydantic v1 backward compatibility and fix linting issues

- Remove all try/except blocks providing Pydantic v1 fallback support
- Update imports to use only Pydantic v2 (TypeAdapter instead of parse_obj_as)
- Clean up TypeAdapter usage throughout codebase
- Fix syntax errors and whitespace issues
- Maintain all existing functionality with Pydantic v2 patterns

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>

* Increment component versions: API 0.24.5->0.25.0, Airlock Processor 0.8.6->0.9.0

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>

* Fix linting issues: remove unused imports from template route files

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>

* Fix unit test errors: remove double .model_dump() calls in test_workspaces.py

Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>

* WIP

* Remove some of the compatibility code.

* Switch to User objects

* Simplify serialization.

* Add tests back

* update tests

* Updates to simplify.

* Updates to simplify code.

* Fix user  model

* Update models to correct syntax for v2

* Address comments.

* Update pydantic version and refactor user fields to maintain backward compatability with v1

* Remove leftover merge marker from changelog

* Fix Pydantic v2 compatibility regressions

- add explicit defaults for nullable model fields
- restore User-to-dict conversion for persisted resource actors
- migrate remaining serialization to model_dump()
- fix nested Event Grid payload serialization
- restore removed resource history behavior and route imports
- preserve legacy role ID, optional email, and cost date handling
- update tests for Pydantic v2 response types and error messages

* Bump version to 0.8.12 for airlock_processor and 0.25.28 for api_app

* Remove unnecessary newline at the end of ResourceTemplate and RestrictedResource classes

* Apply suggestions from code review

Good catch from copilot, we can assume that v1 and v2 will not be installed at the same time and that for imports - this is superfluous

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* - Refactor resource models to use default_factory for list and dict fields as per copilot recomendations.
- Fix indentation

* Refactor schema fields to use default_factory for list and dict types

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Refactor response models to support union types and update default values for dict fields using default_factory

* Refactor user field in Operation and Resource models to use default_factory for dict types

* Refactor validation logic in AirlockRequestRepository and ResourceRepository to remove Pydantic version checks; update test to use actual user ID.

* update to fix miss handeling of model_dump

* add handelling for null values

* Fix OpenAPI schema generation under Pydantic v2

Serialize Property model instances in template response examples to plain
dicts so they don't leak into json_schema_extra and break schema generation
(TypeError: unhashable type: 'Property'). Add a regression test that renders
the full OpenAPI schema.

* Refactor AirlockRequest and AirlockReview models to remove default values for decision fields; update tests to include request type in mock responses.

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* bump api version after it was reset during a rebase

* Refactor AuthenticatedUser model to use ConfigDict for immutability; update tests for role mutation checks and enhance schema service tests for recursive null property removal.

* Fix template schema validation errors and bump affected bundle versions

The API started failing workspace service creation with SchemaError after dependency upgrades.
Root cause was legacy property-level JSON Schema $id fragments (for example "#/properties/..."),
which are rejected by newer jsonschema metaschema validation.

Changes:
- Removed legacy nested property-level $id entries from updated template_schema.json files.
- Added defensive schema normalization in API validation to strip invalid nested $id fragments
  before jsonschema.validate, while preserving top-level schema metadata.
- Added a regression test to ensure legacy nested $id values no longer cause SchemaError.
- Bumped porter bundle versions (patch increments) for every template whose template_schema.json
  was modified, including shared services, workspace services, and guacamole user resource bundles.

Why:
- Restore reliability of template input validation for create/update flows.
- Keep compatibility with already-registered/legacy template content during rollout.
- Keep template bundle versioning consistent with schema changes so upgrades are traceable.

* Handle user model serialization gracefully in resource update

* [resources.py (line 50)](/workspaces/AzureTRE/api_app/db/repositories/resources.py:50) now removes any nested $id with a non-empty URI fragment, including #/properties/..., #properties/..., and absolute URI fragments.
Root $id, valid nested IDs, and the original schema object remain unchanged.
[test_resource_repository.py (line 413)](/workspaces/AzureTRE/api_app/tests_ma/test_db/test_repositories/test_resource_repository.py:413) covers all three invalid forms, including the exact firewall value.

* fix: restore ResourceTemplate.properties type to Dict[str, Property]

properties was changed from Dict[str, Property] to Dict[str, Any] to fix a
jsonschema.SchemaError caused by the legacy Property model serialising optional
fields as null (e.g. "items": null), which is invalid in JSON Schema.

Three changes make Dict[str, Property] safe again:

- Property.model_config adds extra="allow" so unknown JSON Schema keywords
  ($ref, oneOf, format, if/then/else, etc.) are preserved rather than silently
  dropped on deserialisation.
- Property type field is made Optional[str] so properties that use $ref or
  const without an explicit type are accepted.
- Property gains a @model_serializer(mode='plain') that emits only explicitly-
  set fields (model_fields_set), excludes None values, and recurses into nested
  plain-dict sub-schemas (items, properties) to strip any legacy null values.
  A hasattr guard handles the edge case where Pydantic calls the serialiser with
  an uncoerced plain dict due to item-level dict assignment bypassing
  validate_assignment.

ResourceTemplate gains validate_assignment=True so direct field assignment
coerces dict values to Property instances, and a @model_serializer(mode='wrap')
that calls _strip_none_recursive on the full serialised output to cover allOf
and other plain-dict fields that Pydantic's exclude_none does not recurse into.

The legacy remove_legacy_null_property_fields function and its LEGACY_NULL_PROPERTY_FIELDS
allowlist in schema_service are removed; null sanitisation is now owned by the
model layer.

The test was reproducing that step to put the mock enriched_template_mock.return_value into the same state it would be in after enrich_template had run.

Now that ResourceTemplate._serialize calls _strip_none_recursive, allOf: None is stripped during model_dump() itself — so neither the guard in enrich_template nor the pop in the test is needed. The pop("allOf", None) is now a no-op and can be removed entirely

* Fix, PR comments and feedback

* fix: update AirlockRequest and Operation models to use Optional types and set default status

* fix defaults

* update schema examples to present correct types.

* fix: set default value for previous_status in RequestProperties and update enum type in Property model

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Updated [resource.py (line 113)](/workspaces/AzureTRE/api_app/models/domain/resource.py:113) so value is required while explicit JSON null remains valid:

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Restore legacy airlock request type compatibility

Co-authored-by: ChrisChapman-gh <118748128+ChrisChapman-gh@users.noreply.github.com>

* enhance schema normalization to ignore legacy const:null for non-nullable types so validations are clean

* Enhance resource validation by removing accidental const:null constraints on non-nullable properties and updating template normalization logic - app was giving 400 errors and UI would fail when submitting.

* revert servicebus update

* remove unused def

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com>
Co-authored-by: Marcus Robinson <marrobi@microsoft.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.

Migration to Pydantic v2: Later versions of FastAPI require Pydantic v2

6 participants