66
77from typing import Any , Dict , List , Tuple , Type
88
9+ from pydantic .fields import FieldInfo
10+
911from .comparable_field import ComparableField
1012from .comparator_registry import create_comparator
1113
@@ -450,7 +452,7 @@ def _handle_array_type(
450452 return field_type , field
451453
452454
453- def field_to_property (self , field_type : Type , field_info ) -> Dict [str , Any ]:
455+ def field_to_property (self , field_type : Type , field_info : FieldInfo ) -> Dict [str , Any ]:
454456 """Convert Pydantic field to JSON Schema property.
455457
456458 Extracts comparison metadata from the field's json_schema_extra attribute
@@ -462,37 +464,26 @@ def field_to_property(self, field_type: Type, field_info) -> Dict[str, Any]:
462464
463465 Returns:
464466 JSON Schema property dict with x-aws-stickler-* extensions
465-
466- Example:
467- >>> converter = JsonSchemaFieldConverter(schema={}, field_path="")
468- >>> field_info = model.model_fields["name"]
469- >>> property_schema = converter.field_to_property(str, field_info)
470- >>> print(property_schema["x-aws-stickler-comparator"])
471- 'LevenshteinComparator'
472467 """
473- # Use reverse mapping to avoid duplicating type definitions
474468 json_type = PYTHON_TYPE_TO_JSON_TYPE .get (field_type , "string" )
475-
476469 property_schema = {"type" : json_type }
477470
478- # Extract metadata from field
471+ # Extract metadata and build extensions using consolidated helper
479472 metadata = self ._extract_field_metadata (field_info )
473+ extensions = self ._build_comparison_extensions (metadata , format = "json_schema" )
474+ property_schema .update (extensions )
480475
481- # Add x-aws-stickler-* extensions only if metadata exists
482- if metadata .get ("comparator" ):
483- property_schema ["x-aws-stickler-comparator" ] = metadata ["comparator" ].__class__ .__name__
484- if "threshold" in metadata :
485- property_schema ["x-aws-stickler-threshold" ] = metadata ["threshold" ]
486- if "weight" in metadata :
487- property_schema ["x-aws-stickler-weight" ] = metadata ["weight" ]
488- if metadata .get ("clip_under_threshold" ) is not None :
489- property_schema ["x-aws-stickler-clip-under-threshold" ] = metadata ["clip_under_threshold" ]
490- if metadata .get ("aggregate" ):
491- property_schema ["x-aws-stickler-aggregate" ] = metadata ["aggregate" ]
476+ # Add Pydantic field params
477+ if field_info .description :
478+ property_schema ["description" ] = field_info .description
479+ if field_info .alias :
480+ property_schema ["alias" ] = field_info .alias
481+ if field_info .examples :
482+ property_schema ["examples" ] = field_info .examples
492483
493484 return property_schema
494485
495- def field_to_stickler_config (self , field_type : Type , field_info ) -> Dict [str , Any ]:
486+ def field_to_stickler_config (self , field_type : Type , field_info : FieldInfo ) -> Dict [str , Any ]:
496487 """Convert Pydantic field to Stickler config format.
497488
498489 Extracts comparison metadata and formats it as custom Stickler configuration
@@ -504,54 +495,81 @@ def field_to_stickler_config(self, field_type: Type, field_info) -> Dict[str, An
504495
505496 Returns:
506497 Stickler field config dict with type, comparator, threshold, etc.
507-
508- Example:
509- >>> converter = JsonSchemaFieldConverter(schema={}, field_path="")
510- >>> field_info = model.model_fields["name"]
511- >>> config = converter.field_to_stickler_config(str, field_info)
512- >>> print(config["type"])
513- 'str'
514498 """
515- # Use reverse mapping for Stickler type strings
516499 stickler_type = PYTHON_TYPE_TO_STICKLER_TYPE .get (field_type , "str" )
517-
518500 field_config = {"type" : stickler_type }
519501
520- # Extract metadata
502+ # Extract metadata and build extensions using consolidated helper
521503 metadata = self ._extract_field_metadata (field_info )
504+ extensions = self ._build_comparison_extensions (metadata , format = "stickler_config" )
505+ field_config .update (extensions )
522506
523- # Add comparison config only if metadata exists
524- if metadata .get ("comparator" ):
525- field_config ["comparator" ] = metadata ["comparator" ].__class__ .__name__
526- if "threshold" in metadata :
527- field_config ["threshold" ] = metadata ["threshold" ]
528- if "weight" in metadata :
529- field_config ["weight" ] = metadata ["weight" ]
530- if metadata .get ("clip_under_threshold" ) is not None :
531- field_config ["clip_under_threshold" ] = metadata ["clip_under_threshold" ]
532- if metadata .get ("aggregate" ):
533- field_config ["aggregate" ] = metadata ["aggregate" ]
534-
535- # Add Pydantic field params - use is_required() for Pydantic compatibility
507+ # Add Pydantic field params
536508 field_config ["required" ] = field_info .is_required ()
537509 if not field_info .is_required ():
538510 field_config ["default" ] = field_info .default
539511 if field_info .description :
540512 field_config ["description" ] = field_info .description
513+ if field_info .alias :
514+ field_config ["alias" ] = field_info .alias
515+ if field_info .examples :
516+ field_config ["examples" ] = field_info .examples
541517
542518 return field_config
543519
544- def _extract_field_metadata (self , field_info ) -> Dict [str , Any ]:
520+ def _build_comparison_extensions (
521+ self ,
522+ metadata : Dict [str , Any ],
523+ format : str = "json_schema"
524+ ) -> Dict [str , Any ]:
525+ """Build comparison extensions in specified format.
526+
527+ Consolidates duplicate logic from field_to_property() and field_to_stickler_config().
528+
529+ Args:
530+ metadata: Extracted field metadata from _extract_field_metadata()
531+ format: Output format - "json_schema" or "stickler_config"
532+
533+ Returns:
534+ Dictionary with comparison extensions in the specified format
535+ """
536+ extensions = {}
537+ prefix = "x-aws-stickler-" if format == "json_schema" else ""
538+
539+ # Export comparator class name and configuration
540+ if metadata .get ("comparator" ):
541+ comparator = metadata ["comparator" ]
542+ extensions [f"{ prefix } comparator" ] = comparator .__class__ .__name__
543+
544+ # Export comparator configuration (e.g., tolerance, case_sensitive)
545+ if hasattr (comparator , "config" ) and comparator .config :
546+ config_key = f"{ prefix } comparator-config" if format == "json_schema" else "comparator_config"
547+ extensions [config_key ] = comparator .config
548+
549+ # Export comparison parameters
550+ if "threshold" in metadata :
551+ extensions [f"{ prefix } threshold" ] = metadata ["threshold" ]
552+ if "weight" in metadata :
553+ extensions [f"{ prefix } weight" ] = metadata ["weight" ]
554+ if metadata .get ("clip_under_threshold" ) is not None :
555+ clip_key = f"{ prefix } clip-under-threshold" if format == "json_schema" else "clip_under_threshold"
556+ extensions [clip_key ] = metadata ["clip_under_threshold" ]
557+ if metadata .get ("aggregate" ) is not None :
558+ extensions [f"{ prefix } aggregate" ] = metadata ["aggregate" ]
559+
560+ return extensions
561+
562+ def _extract_field_metadata (self , field_info : FieldInfo ) -> Dict [str , Any ]:
545563 """Extract comparison metadata from field's json_schema_extra.
546564
547- The metadata is stored as attributes on the json_schema_extra function
548- by ComparableField() to work around Pydantic's __slots__ restriction.
565+ Only includes attributes that are explicitly set (no default values).
549566
550567 Args:
551568 field_info: Pydantic FieldInfo object
552569
553570 Returns:
554- Dictionary with comparator, threshold, weight, etc.
571+ Dictionary with explicitly set comparator, threshold, weight, etc.
572+ Empty dict if no metadata found.
555573 """
556574 if not hasattr (field_info , "json_schema_extra" ):
557575 return {}
@@ -560,11 +578,22 @@ def _extract_field_metadata(self, field_info) -> Dict[str, Any]:
560578 if not callable (json_func ):
561579 return {}
562580
563- # Extract stored attributes from the function object
564- return {
565- "comparator" : getattr (json_func , "_comparator_instance" , None ),
566- "threshold" : getattr (json_func , "_threshold" , 0.5 ),
567- "weight" : getattr (json_func , "_weight" , 1.0 ),
568- "clip_under_threshold" : getattr (json_func , "_clip_under_threshold" , True ),
569- "aggregate" : getattr (json_func , "_aggregate" , False ),
570- }
581+ # Only include attributes that are explicitly set
582+ metadata = {}
583+
584+ if hasattr (json_func , "_comparator_instance" ):
585+ metadata ["comparator" ] = json_func ._comparator_instance
586+
587+ if hasattr (json_func , "_threshold" ):
588+ metadata ["threshold" ] = json_func ._threshold
589+
590+ if hasattr (json_func , "_weight" ):
591+ metadata ["weight" ] = json_func ._weight
592+
593+ if hasattr (json_func , "_clip_under_threshold" ):
594+ metadata ["clip_under_threshold" ] = json_func ._clip_under_threshold
595+
596+ if hasattr (json_func , "_aggregate" ):
597+ metadata ["aggregate" ] = json_func ._aggregate
598+
599+ return metadata
0 commit comments