|
41 | 41 | is_not_required, |
42 | 42 | is_readonly, |
43 | 43 | is_required, |
| 44 | + is_self, |
44 | 45 | is_special_typing_primitive, |
45 | 46 | is_type_alias_type, |
46 | 47 | is_type_var, |
@@ -364,52 +365,69 @@ def override_with_any(reason: Any) -> None: |
364 | 365 |
|
365 | 366 | @register |
366 | 367 | def on_dataclass(instance: Instance, ctx: Context) -> Optional[JSONSchema]: |
367 | | - # TODO: Self references might not work |
368 | 368 | if is_dataclass(instance.origin_type): |
| 369 | + # When dataclasses reference themselves (typing.Self) or each other, |
| 370 | + # we must break infinite recursion by forcing $ref/$defs. |
| 371 | + origin = instance.origin_type |
| 372 | + |
369 | 373 | if ctx.all_refs: |
370 | | - title = clean_id(type_name(instance.type, short=True)) |
371 | | - title = title.strip("_") |
| 374 | + def_key = clean_id(type_name(instance.type, short=True)).strip("_") |
372 | 375 | else: |
373 | | - title = instance.origin_type.__name__ |
374 | | - jsonschema_config = instance.get_self_config().json_schema |
375 | | - schema = JSONObjectSchema( |
376 | | - title=title, |
377 | | - additionalProperties=jsonschema_config.get( |
378 | | - "additionalProperties", False |
379 | | - ), |
380 | | - ) |
381 | | - properties: dict[str, JSONSchema] = {} |
382 | | - required = [] |
383 | | - field_schema_overrides = jsonschema_config.get("properties", {}) |
384 | | - for f_name, f_type, has_default, f_default in instance.fields(): |
385 | | - override = field_schema_overrides.get(f_name) |
386 | | - f_instance = instance.derive(type=f_type, name=f_name) |
387 | | - if override: |
388 | | - f_schema = JSONSchema.from_dict(override) |
| 376 | + def_key = origin.__name__ |
| 377 | + |
| 378 | + ref_prefix = ctx.ref_prefix or ctx.dialect.definitions_root_pointer |
| 379 | + |
| 380 | + if origin in ctx._building_dataclasses: |
| 381 | + # Ensure placeholder exists so the final schema can fill it in. |
| 382 | + ctx.definitions.setdefault(def_key, EmptyJSONSchema()) |
| 383 | + return JSONSchema(reference=f"{ref_prefix}/{def_key}") |
| 384 | + |
| 385 | + ctx._building_dataclasses.add(origin) |
| 386 | + try: |
| 387 | + # If a placeholder exists (recursion), we'll populate it later |
| 388 | + jsonschema_config = instance.get_self_config().json_schema |
| 389 | + schema = JSONObjectSchema( |
| 390 | + title=def_key, |
| 391 | + additionalProperties=jsonschema_config.get( |
| 392 | + "additionalProperties", False |
| 393 | + ), |
| 394 | + ) |
| 395 | + properties: dict[str, JSONSchema] = {} |
| 396 | + required = [] |
| 397 | + field_schema_overrides = jsonschema_config.get("properties", {}) |
| 398 | + for f_name, f_type, has_default, f_default in instance.fields(): |
| 399 | + override = field_schema_overrides.get(f_name) |
| 400 | + f_instance = instance.derive(type=f_type, name=f_name) |
| 401 | + if override: |
| 402 | + f_schema = JSONSchema.from_dict(override) |
| 403 | + else: |
| 404 | + f_schema = get_schema(f_instance, ctx) |
| 405 | + if f_instance.alias: |
| 406 | + f_name = f_instance.alias |
| 407 | + if f_default is not MISSING: |
| 408 | + f_schema.default = f_default |
| 409 | + description = f_instance.metadata.get("description") |
| 410 | + if description: |
| 411 | + f_schema.description = description |
| 412 | + |
| 413 | + if not has_default: |
| 414 | + required.append(f_name) |
| 415 | + |
| 416 | + properties[f_name] = f_schema |
| 417 | + if properties: |
| 418 | + schema.properties = properties |
| 419 | + if required: |
| 420 | + schema.required = required |
| 421 | + |
| 422 | + # If recursion was detected, we need $defs/$ref regardless |
| 423 | + existing = ctx.definitions.get(def_key) |
| 424 | + if ctx.all_refs or isinstance(existing, EmptyJSONSchema): |
| 425 | + ctx.definitions[def_key] = schema |
| 426 | + return JSONSchema(reference=f"{ref_prefix}/{def_key}") |
389 | 427 | else: |
390 | | - f_schema = get_schema(f_instance, ctx) |
391 | | - if f_instance.alias: |
392 | | - f_name = f_instance.alias |
393 | | - if f_default is not MISSING: |
394 | | - f_schema.default = f_default |
395 | | - description = f_instance.metadata.get("description") |
396 | | - if description: |
397 | | - f_schema.description = description |
398 | | - |
399 | | - if not has_default: |
400 | | - required.append(f_name) |
401 | | - |
402 | | - properties[f_name] = f_schema |
403 | | - if properties: |
404 | | - schema.properties = properties |
405 | | - if required: |
406 | | - schema.required = required |
407 | | - if ctx.all_refs: |
408 | | - ctx.definitions[title] = schema |
409 | | - ref_prefix = ctx.ref_prefix or ctx.dialect.definitions_root_pointer |
410 | | - return JSONSchema(reference=f"{ref_prefix}/{title}") |
411 | | - else: |
412 | | - return schema |
| 428 | + return schema |
| 429 | + finally: |
| 430 | + ctx._building_dataclasses.discard(origin) |
413 | 431 |
|
414 | 432 |
|
415 | 433 | @register |
@@ -466,8 +484,16 @@ def on_special_typing_primitive( |
466 | 484 | ) |
467 | 485 | elif is_literal(instance.type): |
468 | 486 | return on_literal(instance, ctx) |
469 | | - # elif is_self(instance.type): |
470 | | - # raise NotImplementedError |
| 487 | + elif is_self(instance.type): |
| 488 | + # typing.Self / typing_extensions.Self is only meaningful inside |
| 489 | + # a class body. In dataclasses, Instance.owner_class points to the |
| 490 | + # dataclass that defines the field. |
| 491 | + owner = instance.owner_class |
| 492 | + if owner is None: # pragma: no cover |
| 493 | + raise NotImplementedError( |
| 494 | + "typing.Self is supported only for dataclass fields" |
| 495 | + ) |
| 496 | + return get_schema(instance.derive(type=owner), ctx) |
471 | 497 | elif is_required(instance.type) or is_not_required(instance.type): |
472 | 498 | return get_schema(instance.derive(type=args[0]), ctx) |
473 | 499 | elif is_unpack(instance.type): |
|
0 commit comments