11import json
22import re
33import subprocess
4+ from collections import defaultdict
45from pathlib import Path
56from textwrap import dedent
67from typing import (
@@ -529,6 +530,9 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
529530 # lambda x: ... vs lambda _: {}
530531 needs_binding = False
531532 encoder_names = set ()
533+ # Track effective field names to detect collisions after normalization
534+ # Maps effective name -> list of original field names
535+ effective_field_names : defaultdict [str , list [str ]] = defaultdict (list )
532536 if type .properties :
533537 needs_binding = True
534538 typeddict_encoder .append ("{" )
@@ -658,19 +662,37 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
658662 value = ""
659663 if base_model != "TypedDict" :
660664 value = f"= { field_value } "
665+ # Track $kind -> "kind" mapping for collision detection
666+ effective_field_names ["kind" ].append (name )
667+
661668 current_chunks .append (
662669 f" kind: Annotated[{ render_type_expr (type_name )} , Field(alias={
663670 repr (name )
664671 } )]{ value } "
665672 )
666673 else :
674+ specialized_name = normalize_special_chars (name )
675+ effective_name = name
676+ extras = []
677+ if name != specialized_name :
678+ if base_model != "BaseModel" :
679+ # TODO: alias support for TypedDict
680+ raise ValueError (
681+ f"Field { name } is not a valid Python identifier, but it is in the schema" # noqa: E501
682+ )
683+ # Pydantic doesn't allow leading underscores in field names
684+ effective_name = specialized_name .lstrip ("_" )
685+ extras .append (f"alias={ repr (name )} " )
686+
687+ effective_field_names [effective_name ].append (name )
688+
667689 if name not in type .required :
668690 if base_model == "TypedDict" :
669691 current_chunks .append (
670692 reindent (
671693 " " ,
672694 f"""\
673- { name } : NotRequired[{
695+ { effective_name } : NotRequired[{
674696 render_type_expr (
675697 UnionTypeExpr ([type_name , NoneTypeExpr ()])
676698 )
@@ -679,11 +701,13 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
679701 )
680702 )
681703 else :
704+ extras .append ("default=None" )
705+
682706 current_chunks .append (
683707 reindent (
684708 " " ,
685709 f"""\
686- { name } : {
710+ { effective_name } : {
687711 render_type_expr (
688712 UnionTypeExpr (
689713 [
@@ -692,28 +716,30 @@ def extract_props(tpe: RiverType) -> list[dict[str, RiverType]]:
692716 ]
693717 )
694718 )
695- } = None
719+ } = Field( { ", " . join ( extras ) } )
696720 """ ,
697721 )
698722 )
699723 else :
700- specialized_name = normalize_special_chars (name )
701- effective_name = name
702- extras = ""
703- if name != specialized_name :
704- if base_model != "BaseModel" :
705- # TODO: alias support for TypedDict
706- raise ValueError (
707- f"Field { name } is not a valid Python identifier, but it is in the schema" # noqa: E501
708- )
709- # Pydantic doesn't allow leading underscores in field names
710- effective_name = specialized_name .lstrip ("_" )
711- extras = f" = Field(serialization_alias={ repr (name )} )"
724+ extras_str = ""
725+ if len (extras ) != 0 :
726+ extras_str = f" = Field({ ', ' .join (extras )} )"
712727
713728 current_chunks .append (
714- f" { effective_name } : { render_type_expr (type_name )} { extras } "
729+ f" { effective_name } : { render_type_expr (type_name )} { extras_str } " # noqa: E501
715730 )
716731 typeddict_encoder .append ("," )
732+
733+ # Check for field name collisions after processing all fields
734+ for effective_name , original_names in effective_field_names .items ():
735+ if len (original_names ) > 1 :
736+ error_msg = (
737+ f"Field name collision: fields { original_names } all normalize "
738+ f"to the same effective name '{ effective_name } '"
739+ )
740+
741+ raise ValueError (error_msg )
742+
717743 typeddict_encoder .append ("}" )
718744 # exclude_none
719745 typeddict_encoder = (
0 commit comments