@@ -70,6 +70,63 @@ def rewrite_refs(obj, current_schema_rel_path: Path):
7070 return obj
7171
7272
73+ def flatten_validation_oneof (schema : dict ) -> dict :
74+ """Flatten anyOf/oneOf that only express required-field constraints.
75+
76+ JSON Schema uses anyOf/oneOf with required-only branches to express
77+ "at least one of these field groups must be set." datamodel-code-generator
78+ misinterprets this as a type union, generating separate variant classes
79+ (e.g., FrequencyCap1, FrequencyCap2, FrequencyCap3) plus a RootModel wrapper.
80+
81+ This function detects the pattern and removes the anyOf/oneOf, keeping
82+ only the intersection of required fields so a single class is generated.
83+
84+ Follow-up to #155: enables consumer subclassing without RootModel or
85+ Union type alias barriers.
86+ """
87+ if "properties" not in schema :
88+ return schema
89+
90+ branch_key = None
91+ branches = None
92+ for key in ("anyOf" , "oneOf" ):
93+ if key in schema :
94+ branch_key = key
95+ branches = schema [key ]
96+ break
97+
98+ if not branches :
99+ return schema
100+
101+ # All branches must contain only 'required' (and optionally 'not')
102+ if not all (set (b .keys ()) <= {"required" , "not" } for b in branches ):
103+ return schema
104+
105+ # All branches are required-only — this is a validation constraint, not a type union
106+ # Compute the intersection of required fields (fields required in ALL branches)
107+ branch_required = [set (b .get ("required" , [])) for b in branches ]
108+ always_required = set .intersection (* branch_required ) if branch_required else set ()
109+
110+ # Include any top-level required fields
111+ top_required = set (schema .get ("required" , []))
112+ always_required |= top_required
113+
114+ title = schema .get ("title" , "unknown" )
115+ branch_count = len (branches )
116+
117+ # Remove the anyOf/oneOf
118+ del schema [branch_key ]
119+
120+ # Set required to the intersection (or remove if empty)
121+ if always_required :
122+ schema ["required" ] = sorted (always_required )
123+ elif "required" in schema :
124+ del schema ["required" ]
125+
126+ print (f" flattened { branch_key } ({ branch_count } branches) in { title } " )
127+ return schema
128+
129+
73130def flatten_schemas ():
74131 """
75132 Copy schemas to temp directory, preserving directory structure.
@@ -113,6 +170,9 @@ def flatten_schemas():
113170 # Rewrite $ref paths: convert absolute paths to relative, hyphens to underscores
114171 schema = rewrite_refs (schema , rel_path )
115172
173+ # Flatten validation-only anyOf/oneOf into single-class schemas
174+ schema = flatten_validation_oneof (schema )
175+
116176 with open (output_file , "w" ) as f :
117177 json .dump (schema , f , indent = 2 )
118178
@@ -361,6 +421,16 @@ def main():
361421 restore_unchanged_files ()
362422
363423 # Generate ergonomic coercion module (type coercion for better API ergonomics)
424+ # Reset _ergonomic.py first — the old version may import variant classes
425+ # that no longer exist after schema flattening (e.g., PackageUpdate1).
426+ ergonomic_file = REPO_ROOT / "src" / "adcp" / "types" / "_ergonomic.py"
427+ if ergonomic_file .exists ():
428+ ergonomic_file .write_text (
429+ '"""Auto-generated ergonomic coercion — regenerating..."""\n '
430+ "\n def apply_ergonomic_coercion() -> None:\n "
431+ " pass\n "
432+ )
433+
364434 ergonomic_script = REPO_ROOT / "scripts" / "generate_ergonomic_coercion.py"
365435 if ergonomic_script .exists ():
366436 print ("\n Generating ergonomic coercion module..." )
0 commit comments