René's URL Explorer Experiment


Title: Serialize models with `exclude_unset`, not `exclude_defaults` · Issue #207 · crossplane/function-sdk-python · GitHub

Open Graph Title: Serialize models with `exclude_unset`, not `exclude_defaults` · Issue #207 · crossplane/function-sdk-python

X Title: Serialize models with `exclude_unset`, not `exclude_defaults` · Issue #207 · crossplane/function-sdk-python

Description: What happened? resource.update (and resource.update_status) serialize Pydantic models with model_dump(exclude_defaults=True). I think exclude_unset=True is more correct, and a recent change to the generated models has turned this from an...

Open Graph Description: What happened? resource.update (and resource.update_status) serialize Pydantic models with model_dump(exclude_defaults=True). I think exclude_unset=True is more correct, and a recent change to the ...

X Description: What happened? resource.update (and resource.update_status) serialize Pydantic models with model_dump(exclude_defaults=True). I think exclude_unset=True is more correct, and a recent change to the ...

Opengraph URL: https://github.com/crossplane/function-sdk-python/issues/207

X: @github

direct link

Domain: github.com


Hey, it has json ld scripts:
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Serialize models with `exclude_unset`, not `exclude_defaults`","articleBody":"### What happened?\n\n`resource.update` (and `resource.update_status`) serialize Pydantic models with `model_dump(exclude_defaults=True)`. I think `exclude_unset=True` is more correct, and a recent change to the generated models has turned this from an academic distinction into a behavioural regression.\n\nSome background on how we got here. When I added `resource.update` in #98 I reached for `exclude_defaults` to stop `model_dump` serializing every optional field the function didn't set as an explicit `None`. That mostly worked, but it was too aggressive: in #114 we found it also strips `apiVersion` and `kind` when they equal their default, which for a generated model they always do, so we re-add them by hand. I floated switching to `exclude_none` and dropping the defaults from the generated models instead, but @haarchri pointed out that'd cost you the default values in editor/LSP completion, so we kept `exclude_defaults` plus the workaround. We didn't consider `exclude_unset` as an option.\n\nThe Crossplane CLI generates these models with `datamodel-code-generator`, and we're bumping the pinned image from a very old `0.31.2` to `0.59.0` to fix https://github.com/crossplane/cli/issues/63. Along the way the generator changed how it emits a field with an *object* default. Before:\n\n```python\nproviderConfigRef: Optional[ProviderConfigRef] = Field(\n    default_factory=lambda: ProviderConfigRef.model_validate(\n        {'kind': 'ClusterProviderConfig', 'name': 'default'}\n    )\n)\n```\n\nAfter:\n\n```python\nproviderConfigRef: ProviderConfigRef | None = Field(\n    {'kind': 'ClusterProviderConfig', 'name': 'default'}, validate_default=True\n)\n```\n\nThat change is deliberate upstream: see [koxudaxi/datamodel-code-generator#2228](https://github.com/koxudaxi/datamodel-code-generator/issues/2228).\n\nThe trouble is how that interacts with `exclude_defaults`. The two forms behave differently:\n\n- `default_factory`: an unset field is recognized as equal to its default, so `exclude_defaults=True` omits it. The composed resource has no `spec.providerConfigRef`.\n- raw default + `validate_default=True`: the default is validated into a `ProviderConfigRef` *instance* at construction. `exclude_defaults` compares that instance against the field's declared default, a plain `dict`. They're not equal, so the field is *not* excluded.\n\nSo every composed MR now carries an explicit `spec.providerConfigRef: {kind: ClusterProviderConfig, name: default}` that it previously omitted. Same goes for any field with an object default the function didn't set.\n\nIn most cases this isn't a problem. We generate the Pydantic models from the same OpenAPI schemas the Kubernetes API server uses, so if a function emits the default `providerConfigRef` it's just explicitly specifying what the API server would've defaulted anyway. So it's unlikely merging https://github.com/crossplane/cli/pull/64 would break anyone.\n\nSemantically though, I think `exclude_unset` is the correct option. We serialize these models into a `RunFunctionResponse` as desired resources, and Crossplane treats desired resources as server-side apply fully-specified intent. Under SSA the caller should only specify the fields it has an opinion about and wants to own. If a function doesn't explicitly want `providerConfigRef.name=default`, it shouldn't include it. Emitting it claims ownership of a field the function doesn't actually care about. That could mean spurious field-manager conflicts if some other entity *does* care about it, and it pins the field to the schema default even where the effective server-side default might differ (dynamic defaulting, admission webhooks, version skew between the model and the API server).\n\nThe distinction is:\n\n- `exclude_defaults` asks \"is this field's value different from its default?\"\n- `exclude_unset` asks \"did the caller set this field?\"\n\n`exclude_unset` matches the SSA intent: emit exactly the fields the function set, own those, leave the rest to the API server. It's also immune to the codegen change, because it doesn't care how a default is represented. A field the function didn't touch is absent from `model_fields_set`. That fixes the `providerConfigRef` regression for object defaults, scalar defaults, and any future representation the generator produces.\n\nIt is a behaviour change though, so we should think through the implications.\n\nFirst, `apiVersion` and `kind`. `exclude_unset` does *not* let us drop the manual re-add from #114. When a function builds a model with kwargs (the common case) it doesn't pass `apiVersion`/`kind`, so they're not in `model_fields_set` and `exclude_unset` omits them too. The workaround stays necessary either way.\n\nSecond, fields a function sets *to* their default value. Say a function writes `autoCreate=False` where `False` is the schema default. Today `exclude_defaults` drops it; `exclude_unset` keeps it. I'd argue that's more correct. If you set it, you have an opinion and should own it.\n\nThird, snapshot tests. Any function whose tests assert the exact `RunFunctionResponse` could see diffs if fields start appearing that `exclude_defaults` used to strip. (i.e. Fields the user explicitly set to the default value.)\n\n### How can we reproduce it?\n\nHand-write a model with an object-typed field that has a default, mirroring what the newer `datamodel-code-generator` emits, and dump it without setting that field:\n\n```python\nfrom pydantic import BaseModel, Field\n\nclass ProviderConfigRef(BaseModel):\n    kind: str | None = None\n    name: str\n\nclass Spec(BaseModel):\n    providerConfigRef: ProviderConfigRef | None = Field(\n        {\"kind\": \"ClusterProviderConfig\", \"name\": \"default\"}, validate_default=True\n    )\n\nprint(Spec().model_dump(exclude_defaults=True))\n# {'providerConfigRef': {'kind': 'ClusterProviderConfig', 'name': 'default'}}\nprint(Spec().model_dump(exclude_unset=True))\n# {}\n```\n\nThe `exclude_defaults` dump includes a field the caller never set; `exclude_unset` doesn't.\n\n### What environment did it happen in?\n\nFunction version: n/a — this is about `resource.update`/`update_status` in the SDK itself. The regression surfaces for anyone using models generated by `datamodel-code-generator \u003e= 0.54.0`, where the `validate_default=True` change landed, which the Crossplane CLI will produce once [crossplane/cli#64](https://github.com/crossplane/cli/pull/64) merges.\n","author":{"url":"https://github.com/negz","@type":"Person","name":"negz"},"datePublished":"2026-06-04T00:01:52.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/207/function-sdk-python/issues/207"}

route-pattern/_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format)
route-controllervoltron_issues_fragments
route-actionissue_layout
fetch-noncev2:823c2199-3549-34ef-1dae-e8ba7886c093
current-catalog-service-hash81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114
request-id815C:FBFD3:237602:31A8E0:6A605520
html-safe-nonce55cab31c61c6627f63dfc55f0d7fac72de2141a4fc892b7f466443cf20780d68
visitor-payloadeyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI4MTVDOkZCRkQzOjIzNzYwMjozMUE4RTA6NkE2MDU1MjAiLCJ2aXNpdG9yX2lkIjoiNTE4NDk1ODAzNDE3NjEzNjQ4MCIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9
visitor-hmacae0a85212c788c6afd43640b206afe6c6343bfac1b7f6318d13c66bf1882c0fb
hovercard-subject-tagissue:4584584320
github-keyboard-shortcutsrepository,issues,copilot
google-site-verificationApib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I
octolytics-urlhttps://collector.github.com/github/collect
analytics-location///voltron/issues_fragments/issue_layout
fb:app_id1401488693436528
apple-itunes-appapp-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/crossplane/function-sdk-python/207/issue_layout
twitter:imagehttps://opengraph.githubassets.com/ea7dd8d33cc9c13e1e1f2f98e7db26f58d25da4042964d2fe2dd87c9dcccb776/crossplane/function-sdk-python/issues/207
twitter:cardsummary_large_image
og:imagehttps://opengraph.githubassets.com/ea7dd8d33cc9c13e1e1f2f98e7db26f58d25da4042964d2fe2dd87c9dcccb776/crossplane/function-sdk-python/issues/207
og:image:altWhat happened? resource.update (and resource.update_status) serialize Pydantic models with model_dump(exclude_defaults=True). I think exclude_unset=True is more correct, and a recent change to the ...
og:image:width1200
og:image:height600
og:site_nameGitHub
og:typeobject
og:author:usernamenegz
hostnamegithub.com
expected-hostnamegithub.com
None60da8c2a42fa2bbf5f7567474990ec467836a84444262a58e200fa91b7f3d2d0
turbo-cache-controlno-preview
go-importgithub.com/crossplane/function-sdk-python git https://github.com/crossplane/function-sdk-python.git
octolytics-dimension-user_id45158470
octolytics-dimension-user_logincrossplane
octolytics-dimension-repository_id721365259
octolytics-dimension-repository_nwocrossplane/function-sdk-python
octolytics-dimension-repository_publictrue
octolytics-dimension-repository_is_forkfalse
octolytics-dimension-repository_network_root_id721365259
octolytics-dimension-repository_network_root_nwocrossplane/function-sdk-python
turbo-body-classeslogged-out env-production page-responsive
disable-turbofalse
browser-stats-urlhttps://api.github.com/_private/browser/stats
browser-errors-urlhttps://api.github.com/_private/browser/errors
release9824515e740d83d5eb82168a089b806ab0fe04a1
ui-targetfull
theme-color#1e2327
color-schemelight dark

Links:

Skip to contenthttps://github.com/crossplane/function-sdk-python/issues/207#start-of-content
https://github.com/
Sign in https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fcrossplane%2Ffunction-sdk-python%2Fissues%2F207
GitHub CopilotWrite better code with AIhttps://github.com/features/copilot
GitHub Copilot appDirect agents from issue to mergehttps://github.com/features/ai/github-app
MCP RegistryNewIntegrate external toolshttps://github.com/mcp
ActionsAutomate any workflowhttps://github.com/features/actions
CodespacesInstant dev environmentshttps://github.com/features/codespaces
IssuesPlan and track workhttps://github.com/features/issues
Code ReviewManage code changeshttps://github.com/features/code-review
Code QualityEnforce quality at mergehttps://github.com/features/code-quality
GitHub Advanced SecurityFind and fix vulnerabilitieshttps://github.com/security/advanced-security
Code securitySecure your code as you buildhttps://github.com/security/advanced-security/code-security
Secret protectionStop leaks before they starthttps://github.com/security/advanced-security/secret-protection
Why GitHubhttps://github.com/why-github
Documentationhttps://docs.github.com
Bloghttps://github.blog
Changeloghttps://github.blog/changelog
Marketplacehttps://github.com/marketplace
View all featureshttps://github.com/features
Enterpriseshttps://github.com/enterprise
Small and medium teamshttps://github.com/team
Startupshttps://github.com/enterprise/startups
Nonprofitshttps://github.com/solutions/industry/nonprofits
App Modernizationhttps://github.com/solutions/use-case/app-modernization
DevSecOpshttps://github.com/solutions/use-case/devsecops
DevOpshttps://github.com/solutions/use-case/devops
CI/CDhttps://github.com/solutions/use-case/ci-cd
View all use caseshttps://github.com/solutions/use-case
Healthcarehttps://github.com/solutions/industry/healthcare
Financial serviceshttps://github.com/solutions/industry/financial-services
Manufacturinghttps://github.com/solutions/industry/manufacturing
Governmenthttps://github.com/solutions/industry/government
View all industrieshttps://github.com/solutions/industry
View all solutionshttps://github.com/solutions
AIhttps://github.com/resources/articles?topic=ai
Software Developmenthttps://github.com/resources/articles?topic=software-development
DevOpshttps://github.com/resources/articles?topic=devops
Securityhttps://github.com/resources/articles?topic=security
View all topicshttps://github.com/resources/articles
Customer storieshttps://github.com/customer-stories
Events & webinarshttps://github.com/resources/events
Ebooks & reportshttps://github.com/resources/whitepapers
Business insightshttps://github.com/solutions/executive-insights
GitHub Skillshttps://skills.github.com
Documentationhttps://docs.github.com
Customer supporthttps://support.github.com
Community forumhttps://github.com/orgs/community/discussions
Trust centerhttps://github.com/trust-center
Partnershttps://github.com/partners
View all resourceshttps://github.com/resources
GitHub SponsorsFund open source developershttps://github.com/open-source/sponsors
Security Labhttps://securitylab.github.com
Maintainer Communityhttps://maintainers.github.com
Acceleratorhttps://github.com/open-source/accelerator
GitHub Starshttps://stars.github.com
Archive Programhttps://archiveprogram.github.com
Topicshttps://github.com/topics
Trendinghttps://github.com/trending
Collectionshttps://github.com/collections
Enterprise platformAI-powered developer platformhttps://github.com/enterprise
GitHub Advanced SecurityEnterprise-grade security featureshttps://github.com/security/advanced-security
Copilot for BusinessEnterprise-grade AI featureshttps://github.com/features/copilot/copilot-business
Premium SupportEnterprise-grade 24/7 supporthttps://github.com/enterprise/premium-support
Pricinghttps://github.com/pricing
Search syntax tipshttps://docs.github.com/search-github/github-code-search/understanding-github-code-search-syntax
documentationhttps://docs.github.com/search-github/github-code-search/understanding-github-code-search-syntax
Sign in https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fcrossplane%2Ffunction-sdk-python%2Fissues%2F207
Sign up https://github.com/signup?ref_cta=Sign+up&ref_loc=header+logged+out&ref_page=%2F%3Cuser-name%3E%2F%3Crepo-name%3E%2Fvoltron%2Fissues_fragments%2Fissue_layout&source=header-repo&source_repo=crossplane%2Ffunction-sdk-python
Reloadhttps://github.com/crossplane/function-sdk-python/issues/207
Reloadhttps://github.com/crossplane/function-sdk-python/issues/207
Reloadhttps://github.com/crossplane/function-sdk-python/issues/207
Please reload this pagehttps://github.com/crossplane/function-sdk-python/issues/207
crossplane https://github.com/crossplane
function-sdk-pythonhttps://github.com/crossplane/function-sdk-python
Notifications https://github.com/login?return_to=%2Fcrossplane%2Ffunction-sdk-python
Fork 14 https://github.com/login?return_to=%2Fcrossplane%2Ffunction-sdk-python
Star 13 https://github.com/login?return_to=%2Fcrossplane%2Ffunction-sdk-python
Code https://github.com/crossplane/function-sdk-python
Issues 6 https://github.com/crossplane/function-sdk-python/issues
Pull requests 1 https://github.com/crossplane/function-sdk-python/pulls
Actions https://github.com/crossplane/function-sdk-python/actions
Projects https://github.com/crossplane/function-sdk-python/projects
Security and quality 0 https://github.com/crossplane/function-sdk-python/security
Insights https://github.com/crossplane/function-sdk-python/pulse
Code https://github.com/crossplane/function-sdk-python
Issues https://github.com/crossplane/function-sdk-python/issues
Pull requests https://github.com/crossplane/function-sdk-python/pulls
Actions https://github.com/crossplane/function-sdk-python/actions
Projects https://github.com/crossplane/function-sdk-python/projects
Security and quality https://github.com/crossplane/function-sdk-python/security
Insights https://github.com/crossplane/function-sdk-python/pulse
#208https://github.com/crossplane/function-sdk-python/pull/208
Serialize models with exclude_unset, not exclude_defaultshttps://github.com/crossplane/function-sdk-python/issues/207#top
#208https://github.com/crossplane/function-sdk-python/pull/208
https://github.com/negz
bugSomething isn't workinghttps://github.com/crossplane/function-sdk-python/issues?q=state%3Aopen%20label%3A%22bug%22
https://github.com/negz
negzhttps://github.com/negz
on Jun 4, 2026https://github.com/crossplane/function-sdk-python/issues/207#issue-4584584320
#98https://github.com/crossplane/function-sdk-python/pull/98
#114https://github.com/crossplane/function-sdk-python/pull/114
@haarchrihttps://github.com/haarchri
crossplane/cli#63https://github.com/crossplane/cli/issues/63
koxudaxi/datamodel-code-generator#2228https://github.com/koxudaxi/datamodel-code-generator/issues/2228
crossplane/cli#64https://github.com/crossplane/cli/pull/64
#114https://github.com/crossplane/function-sdk-python/pull/114
crossplane/cli#64https://github.com/crossplane/cli/pull/64
negzhttps://github.com/negz
bugSomething isn't workinghttps://github.com/crossplane/function-sdk-python/issues?q=state%3Aopen%20label%3A%22bug%22
https://github.com
Termshttps://docs.github.com/site-policy/github-terms/github-terms-of-service
Privacyhttps://docs.github.com/site-policy/privacy-policies/github-privacy-statement
Securityhttps://github.com/security
Statushttps://www.githubstatus.com/
Communityhttps://github.community/
Docshttps://docs.github.com/
Contacthttps://support.github.com?tags=dotcom-footer

Viewport: width=device-width


URLs of crawlers that visited me.