pydantic-marshmallow 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,16 +51,22 @@ Features:
51
51
 
52
52
  # Re-export Marshmallow's validators - these work with PydanticSchema
53
53
  # Also export hooks for convenience
54
+ # Note: We re-export Marshmallow's @validates and @validates_schema above.
55
+ # Our bridge's _do_load calls Marshmallow's native validator system,
56
+ # so `from marshmallow import validates` works correctly with PydanticSchema.
57
+ # Version is managed by setuptools-scm from git tags
58
+ from importlib.metadata import version as _version
59
+
54
60
  from marshmallow import EXCLUDE, INCLUDE, RAISE, post_dump, post_load, pre_dump, pre_load, validates, validates_schema
55
61
 
56
62
  from .bridge import HybridModel, PydanticSchema, pydantic_schema, schema_for
57
63
  from .errors import BridgeValidationError
58
64
 
59
- # Note: We re-export Marshmallow's @validates and @validates_schema above.
60
- # Our bridge's _do_load calls Marshmallow's native validator system,
61
- # so `from marshmallow import validates` works correctly with PydanticSchema.
62
-
63
- __version__ = "0.1.0"
65
+ try:
66
+ __version__ = _version("pydantic-marshmallow")
67
+ except Exception:
68
+ # Fallback for development or environments without installed package metadata
69
+ __version__ = "0.0.0.dev0"
64
70
  __all__ = [
65
71
  "EXCLUDE",
66
72
  "INCLUDE",
@@ -7,7 +7,10 @@ Flow: Input → Marshmallow pre_load → PYDANTIC VALIDATES → Marshmallow post
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import threading
10
11
  from collections.abc import Callable, Sequence, Set as AbstractSet
12
+ from functools import lru_cache
13
+ from importlib.metadata import version as get_version
11
14
  from typing import Any, ClassVar, Generic, TypeVar, cast, get_args, get_origin
12
15
 
13
16
  from marshmallow import EXCLUDE, INCLUDE, RAISE, Schema, fields as ma_fields
@@ -15,22 +18,76 @@ from marshmallow.decorators import VALIDATES, VALIDATES_SCHEMA
15
18
  from marshmallow.error_store import ErrorStore
16
19
  from marshmallow.exceptions import ValidationError as MarshmallowValidationError
17
20
  from marshmallow.schema import SchemaMeta
21
+ from marshmallow.utils import missing as ma_missing # type: ignore[attr-defined,unused-ignore]
18
22
  from pydantic import BaseModel, ConfigDict, ValidationError as PydanticValidationError
19
23
 
20
24
  from .errors import BridgeValidationError, convert_pydantic_errors, format_pydantic_error
21
- from .field_conversion import convert_model_fields, convert_pydantic_field
25
+ from .field_conversion import _get_computed_fields, convert_model_fields, convert_pydantic_field
22
26
  from .validators import cache_validators
23
27
 
28
+
29
+ # Marshmallow version detection for context parameter compatibility
30
+ # MA 4.x removes the `context` parameter from Schema.__init__
31
+ def _parse_version(version_str: str) -> tuple[int, int]:
32
+ """Parse version string, handling pre-release versions like '4.0.0rc1'."""
33
+ import re
34
+ match = re.match(r'(\d+)\.(\d+)', version_str)
35
+ if match:
36
+ return (int(match.group(1)), int(match.group(2)))
37
+ raise ValueError(f"Cannot parse marshmallow version: {version_str}") # pragma: no cover
38
+
39
+
40
+ _MARSHMALLOW_VERSION = _parse_version(get_version("marshmallow"))
41
+ _MARSHMALLOW_4_PLUS = _MARSHMALLOW_VERSION >= (4, 0)
42
+
43
+
24
44
  M = TypeVar("M", bound=BaseModel)
25
45
 
46
+ # =============================================================================
47
+ # Module-level Caches
48
+ # =============================================================================
49
+ # Thread Safety: All caches use thread-safe implementations to support both
50
+ # GIL-enabled Python and free-threaded Python 3.14+ (PEP 703).
51
+ # - @lru_cache: Inherently thread-safe (uses internal lock)
52
+ # - Dict caches: Protected by threading.Lock for write operations
53
+ #
54
+ # Cache Invalidation: These caches assume Pydantic models are immutable after
55
+ # definition. Dynamic model modification at runtime (monkey-patching, adding
56
+ # fields) will result in stale cached data. This is an accepted trade-off for
57
+ # performance gains.
58
+ # =============================================================================
59
+
60
+ # Thread lock for cache writes (supports free-threaded Python 3.14+)
61
+ # Use RLock (reentrant) since from_model() may be called recursively
62
+ _cache_lock = threading.RLock()
63
+
26
64
  # Module-level cache for HybridModel schemas
27
65
  _hybrid_schema_cache: dict[type[Any], type[PydanticSchema[Any]]] = {}
28
66
 
67
+ # Cache for schema_for/from_model - key is (model, schema_name, frozen options)
68
+ # This avoids recreating schema classes for the same model+options
69
+ _schema_class_cache: dict[tuple[type[Any], str | None, tuple[tuple[str, Any], ...]], type[Any]] = {}
70
+
29
71
  # Field validator registry: maps (schema_class, field_name) -> list of validator functions
30
72
  _field_validators: dict[tuple[type[Any], str], list[Callable[..., Any]]] = {}
31
73
  _schema_validators: dict[type[Any], list[Callable[..., Any]]] = {}
32
74
 
33
75
 
76
+ @lru_cache(maxsize=1024)
77
+ def _get_model_field_names_with_aliases(model_class: type[BaseModel]) -> frozenset[str]:
78
+ """
79
+ Get cached frozenset of model field names including aliases.
80
+
81
+ Thread-safe via @lru_cache. Caches the result per model class to avoid
82
+ repeated computation in the load() hot path.
83
+ """
84
+ field_names = set(model_class.model_fields.keys())
85
+ for field_info in model_class.model_fields.values():
86
+ if field_info.alias:
87
+ field_names.add(field_info.alias)
88
+ return frozenset(field_names)
89
+
90
+
34
91
  class PydanticSchemaMeta(SchemaMeta):
35
92
  """
36
93
  Custom metaclass that adds Pydantic model fields BEFORE Marshmallow processes them.
@@ -130,10 +187,11 @@ class PydanticSchemaMeta(SchemaMeta):
130
187
  attrs[field_name] = convert_pydantic_field(field_name, field_info)
131
188
 
132
189
  # Add computed fields as dump_only
133
- if hasattr(model_class, 'model_computed_fields'):
190
+ computed_fields = _get_computed_fields(model_class)
191
+ if computed_fields:
134
192
  from .field_conversion import convert_computed_field
135
193
 
136
- for field_name, computed_info in model_class.model_computed_fields.items():
194
+ for field_name, computed_info in computed_fields.items():
137
195
  if field_name in attrs:
138
196
  continue
139
197
  # If attrs has pre-filtered fields, don't add computed fields
@@ -149,7 +207,7 @@ class PydanticSchemaMeta(SchemaMeta):
149
207
  return cast(PydanticSchemaMeta, super().__new__(mcs, name, bases, attrs))
150
208
 
151
209
 
152
- class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
210
+ class _PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
153
211
  """
154
212
  A Marshmallow schema backed by a Pydantic model.
155
213
 
@@ -228,20 +286,27 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
228
286
  self._dump_only_fields: set[str] = set(dump_only) if dump_only else set()
229
287
  self._partial: bool | Sequence[str] | AbstractSet[str] | None = partial
230
288
  self._unknown_override: str | None = unknown
231
- self._context = context or {}
232
289
 
233
- # Pass all known kwargs to parent including context
234
- super().__init__(
235
- only=only,
236
- exclude=exclude,
237
- context=context,
238
- many=many,
239
- load_only=load_only,
240
- dump_only=dump_only,
241
- partial=partial,
242
- unknown=unknown,
290
+ # Build kwargs for super().__init__()
291
+ # MA 4.x removes context parameter - only pass it for MA 3.x
292
+ parent_kwargs: dict[str, Any] = {
293
+ "only": only,
294
+ "exclude": exclude,
295
+ "many": many,
296
+ "load_only": load_only,
297
+ "dump_only": dump_only,
298
+ "partial": partial,
299
+ "unknown": unknown,
243
300
  **kwargs,
244
- )
301
+ }
302
+ if not _MARSHMALLOW_4_PLUS:
303
+ parent_kwargs["context"] = context
304
+
305
+ super().__init__(**parent_kwargs)
306
+
307
+ # For MA 4.x, store context manually if provided
308
+ if _MARSHMALLOW_4_PLUS and context is not None: # pragma: no cover
309
+ self.context = context
245
310
  self._model_class = self._get_model_class()
246
311
  if self._model_class:
247
312
  self._setup_fields_from_model()
@@ -250,7 +315,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
250
315
  for field_name, field_obj in self.fields.items():
251
316
  self.on_bind_field(field_name, field_obj)
252
317
 
253
- def on_bind_field(self, field_name: str, field_obj: ma_fields.Field) -> None:
318
+ def on_bind_field(self, field_name: str, field_obj: Any) -> None:
254
319
  """
255
320
  Hook called when a field is bound to the schema.
256
321
 
@@ -300,16 +365,6 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
300
365
  """
301
366
  raise error
302
367
 
303
- @property
304
- def context(self) -> dict[str, Any]:
305
- """Get the validation context."""
306
- return self._context
307
-
308
- @context.setter
309
- def context(self, value: dict[str, Any]) -> None:
310
- """Set the validation context."""
311
- self._context = value
312
-
313
368
  def _get_model_class(self) -> type[BaseModel] | None:
314
369
  """Get the Pydantic model class from Meta or generic parameter."""
315
370
  # Try Meta.model first
@@ -389,6 +444,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
389
444
  data: dict[str, Any],
390
445
  partial: bool | Sequence[str] | AbstractSet[str] | None = None,
391
446
  original_data: Any | None = None,
447
+ skip_model_dump: bool = False,
392
448
  ) -> tuple[dict[str, Any], M | None]:
393
449
  """
394
450
  Use Pydantic to validate and coerce the input data.
@@ -396,6 +452,13 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
396
452
  Filters out marshmallow.missing sentinel values before Pydantic validation,
397
453
  allowing Pydantic to use its own defaults for missing fields.
398
454
 
455
+ Args:
456
+ data: Input data to validate
457
+ partial: Partial validation mode
458
+ original_data: Original input data for error reporting
459
+ skip_model_dump: If True and not partial, skip model_dump() and return
460
+ empty dict. Use when validators don't need the dict.
461
+
399
462
  Returns:
400
463
  Tuple of (validated_data_dict, model_instance)
401
464
  The instance is returned to avoid redundant validation later.
@@ -404,8 +467,6 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
404
467
  return data, None
405
468
 
406
469
  # Filter out marshmallow.missing values - Pydantic should use its defaults
407
- from marshmallow.utils import missing as ma_missing
408
-
409
470
  clean_data = {
410
471
  k: v for k, v in data.items()
411
472
  if v is not ma_missing
@@ -420,6 +481,9 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
420
481
  else:
421
482
  # Let Pydantic do all the validation - KEEP THE INSTANCE
422
483
  instance = self._model_class.model_validate(clean_data)
484
+ # OPTIMIZATION: Skip model_dump if not needed for validators
485
+ if skip_model_dump:
486
+ return {}, cast(M, instance)
423
487
  # Return both the dict (for validators) and instance (for result)
424
488
  validated_data = instance.model_dump(by_alias=False)
425
489
  # Cast to M since model_validate returns the correct model type
@@ -545,6 +609,11 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
545
609
 
546
610
  This ensures 100% Marshmallow hook compatibility.
547
611
  """
612
+ # PERFORMANCE: Hoist frequently accessed attributes to local variables
613
+ # This avoids repeated self.__dict__ lookups in the hot path
614
+ model_class = self._model_class
615
+ hooks = self._hooks
616
+
548
617
  # Resolve settings
549
618
  if many is None:
550
619
  many = self.many
@@ -562,7 +631,9 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
562
631
  if many:
563
632
  if not isinstance(data, list):
564
633
  raise MarshmallowValidationError({"_schema": ["Expected a list."]})
565
- return [
634
+
635
+ # Process each item individually (validators run per-item with pass_many=False)
636
+ results = [
566
637
  self._do_load(
567
638
  item,
568
639
  many=False,
@@ -574,28 +645,57 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
574
645
  for item in data
575
646
  ]
576
647
 
577
- # Step 1: Run pre_load hooks ONLY if they exist (PERFORMANCE OPTIMIZATION)
578
- # Skipping _invoke_load_processors when empty saves ~5ms per 10k loads
579
- if self._hooks.get("pre_load"):
580
- processed_data = self._invoke_load_processors(
581
- "pre_load",
582
- data,
583
- many=False,
584
- original_data=data,
585
- partial=partial,
586
- )
587
- else:
588
- processed_data = data
648
+ # After all items processed, run @validates_schema(pass_many=True) validators
649
+ # These should see the FULL collection, not individual items
650
+ coll_error_store_cls: Callable[[], Any] = cast(Callable[[], Any], ErrorStore)
651
+ coll_error_store: Any = coll_error_store_cls()
652
+
653
+ # Build kwargs for collection-level validation
654
+ coll_pass_param = "pass_collection" if _MARSHMALLOW_4_PLUS else "pass_many"
655
+ coll_validator_kwargs: dict[str, Any] = {
656
+ "error_store": coll_error_store,
657
+ "data": results,
658
+ "original_data": data,
659
+ "many": True,
660
+ "partial": partial,
661
+ "field_errors": False, # Field errors already handled per-item
662
+ }
663
+ # MA 4.x requires 'unknown' parameter
664
+ if _MARSHMALLOW_4_PLUS: # pragma: no cover
665
+ coll_validator_kwargs["unknown"] = unknown_setting
666
+ # Only run pass_many=True validators (collection-level)
667
+ self._invoke_schema_validators(**{coll_pass_param: True, **coll_validator_kwargs})
668
+
669
+ if coll_error_store.errors:
670
+ error = MarshmallowValidationError(dict(coll_error_store.errors))
671
+ self.handle_error(error, data, many=True)
672
+
673
+ return results
674
+
675
+ # Step 1: Run pre_load hooks
676
+ # MA 4.x requires 'unknown' parameter, MA 3.x doesn't accept it
677
+ pre_load_kwargs: dict[str, Any] = {
678
+ "many": False,
679
+ "original_data": data,
680
+ "partial": partial,
681
+ }
682
+ if _MARSHMALLOW_4_PLUS: # pragma: no cover
683
+ pre_load_kwargs["unknown"] = unknown_setting
684
+ processed_data_raw = self._invoke_load_processors(
685
+ "pre_load",
686
+ data,
687
+ **pre_load_kwargs,
688
+ )
589
689
 
590
- # Step 2: Handle unknown fields based on setting
591
- if self._model_class:
592
- model_fields = set(self._model_class.model_fields.keys())
593
- # Also include aliases in known fields
594
- for _field_name, field_info in self._model_class.model_fields.items():
595
- if field_info.alias:
596
- model_fields.add(field_info.alias)
690
+ # Type narrowing: at this point (many=False path), data is always a dict
691
+ processed_data: dict[str, Any] = cast(dict[str, Any], processed_data_raw)
597
692
 
598
- unkn_fields = set(processed_data.keys()) - model_fields
693
+ # Step 2: Handle unknown fields based on setting
694
+ # PERFORMANCE: Use cached field names instead of computing every time
695
+ model_field_names: frozenset[str] | None = None
696
+ if model_class:
697
+ model_field_names = _get_model_field_names_with_aliases(model_class)
698
+ unkn_fields = set(processed_data.keys()) - model_field_names
599
699
 
600
700
  if unkn_fields:
601
701
  if unknown_setting == RAISE:
@@ -604,17 +704,52 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
604
704
  if unknown_setting == EXCLUDE:
605
705
  # Remove unknown fields
606
706
  processed_data = {
607
- k: v for k, v in processed_data.items() if k in model_fields
707
+ k: v for k, v in processed_data.items() if k in model_field_names
608
708
  }
609
709
  # INCLUDE: keep unknown fields in the result (handled below)
610
710
 
611
711
  # Step 3: Pydantic validates the transformed data
612
712
  # Returns (validated_dict, instance) - instance reused to avoid double validation
613
- pydantic_instance = None
614
- if self._model_class:
713
+ pydantic_instance: M | None = None
714
+ validated_data: dict[str, Any]
715
+
716
+ # OPTIMIZATION: Determine if we need model_dump() for validators/result
717
+ # Skip expensive model_dump() when not needed
718
+ # Note: MA 3.x uses tuple keys ('validates', field) and ('validates_schema', pass_many)
719
+ # MA 4.x uses string keys ('validates', 'validates_schema')
720
+ # Check for any hook that starts with the validator type
721
+ def _has_hook(hook_type: str) -> bool:
722
+ """Check if any hook matches the type (handles both MA 3.x and 4.x key formats)."""
723
+ # Check string key (MA 4.x) - safe to access defaultdict
724
+ if hooks.get(hook_type):
725
+ return True
726
+ # Check tuple keys (MA 3.x) - cast to Any to avoid mypy narrowing issues
727
+ # across different Marshmallow versions with different key types
728
+ hooks_any: dict[Any, Any] = cast(dict[Any, Any], hooks)
729
+ for key in hooks_any:
730
+ if isinstance(key, tuple) and len(key) >= 1 and key[0] == hook_type and hooks_any[key]:
731
+ return True
732
+ return False
733
+
734
+ has_validators = bool(
735
+ _has_hook(VALIDATES)
736
+ or _has_hook(VALIDATES_SCHEMA)
737
+ or self._field_validators_cache
738
+ or self._schema_validators_cache
739
+ )
740
+ needs_dict = (
741
+ not return_instance # Need dict for result
742
+ or unknown_setting == INCLUDE # Need dict to merge unknown fields
743
+ or has_validators # Need dict for validators
744
+ )
745
+
746
+ if model_class:
615
747
  try:
616
748
  validated_data, pydantic_instance = self._validate_with_pydantic(
617
- processed_data, partial=partial, original_data=data
749
+ processed_data,
750
+ partial=partial,
751
+ original_data=data,
752
+ skip_model_dump=not needs_dict,
618
753
  )
619
754
  except MarshmallowValidationError as pydantic_error:
620
755
  # Call handle_error for Pydantic validation errors
@@ -623,8 +758,8 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
623
758
  raise
624
759
 
625
760
  # If INCLUDE, add unknown fields back to validated data
626
- if unknown_setting == INCLUDE and self._model_class:
627
- for field in (set(processed_data.keys()) - model_fields):
761
+ if unknown_setting == INCLUDE and model_field_names is not None:
762
+ for field in (set(processed_data.keys()) - model_field_names):
628
763
  validated_data[field] = processed_data[field]
629
764
  else:
630
765
  validated_data = processed_data
@@ -635,13 +770,12 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
635
770
  error_store_cls: Callable[[], Any] = cast(Callable[[], Any], ErrorStore)
636
771
  error_store: Any = error_store_cls()
637
772
 
638
- # 4a: Run Marshmallow's native @validates decorators (from _hooks)
639
- if self._hooks[VALIDATES]:
640
- self._invoke_field_validators(
641
- error_store=error_store,
642
- data=validated_data,
643
- many=False,
644
- )
773
+ # 4a: Run Marshmallow's native @validates decorators (from hooks)
774
+ self._invoke_field_validators(
775
+ error_store=error_store,
776
+ data=validated_data,
777
+ many=False,
778
+ )
645
779
 
646
780
  # 4b: Run our custom @validates decorators (backwards compatibility)
647
781
  try:
@@ -656,25 +790,25 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
656
790
 
657
791
  # Step 5: Run schema validators (BOTH Marshmallow native AND our custom)
658
792
  # 5a: Run Marshmallow's native @validates_schema decorators
659
- if self._hooks[VALIDATES_SCHEMA]:
660
- self._invoke_schema_validators(
661
- error_store=error_store,
662
- pass_many=True,
663
- data=validated_data,
664
- original_data=data,
665
- many=False,
666
- partial=partial,
667
- field_errors=has_field_errors,
668
- )
669
- self._invoke_schema_validators(
670
- error_store=error_store,
671
- pass_many=False,
672
- data=validated_data,
673
- original_data=data,
674
- many=False,
675
- partial=partial,
676
- field_errors=has_field_errors,
677
- )
793
+ # MA 4.x renamed pass_many -> pass_collection
794
+ # Build common kwargs
795
+ validator_kwargs: dict[str, Any] = {
796
+ "error_store": error_store,
797
+ "data": validated_data,
798
+ "original_data": data,
799
+ "many": False,
800
+ "partial": partial,
801
+ "field_errors": has_field_errors,
802
+ }
803
+ # MA 4.x requires 'unknown' parameter
804
+ if _MARSHMALLOW_4_PLUS: # pragma: no cover
805
+ validator_kwargs["unknown"] = unknown_setting
806
+ # Pass the correctly-named parameter for the marshmallow version
807
+ pass_param = "pass_collection" if _MARSHMALLOW_4_PLUS else "pass_many"
808
+
809
+ # Only run pass_many=False validators in per-item mode
810
+ # pass_many=True validators are handled at the collection level (in many=True block above)
811
+ self._invoke_schema_validators(**{pass_param: False, **validator_kwargs})
678
812
 
679
813
  # 5b: Run our custom @validates_schema decorators (backwards compatibility)
680
814
  try:
@@ -690,7 +824,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
690
824
  self.handle_error(error, data, many=False)
691
825
 
692
826
  # Step 6: Prepare result based on return_instance flag
693
- if self._model_class and return_instance:
827
+ if model_class and return_instance:
694
828
  if not partial:
695
829
  # OPTIMIZATION: Reuse the instance from _validate_with_pydantic
696
830
  result = pydantic_instance if pydantic_instance is not None else validated_data
@@ -701,7 +835,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
701
835
  construct_data = {}
702
836
  fields_set = set()
703
837
 
704
- for field_name, field_info in self._model_class.model_fields.items():
838
+ for field_name, field_info in model_class.model_fields.items():
705
839
  if field_name in validated_data:
706
840
  construct_data[field_name] = validated_data[field_name]
707
841
  fields_set.add(field_name)
@@ -718,20 +852,26 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
718
852
  construct_data[field_name] = None
719
853
 
720
854
  result = cast(
721
- M, self._model_class.model_construct(_fields_set=fields_set, **construct_data)
855
+ M, model_class.model_construct(_fields_set=fields_set, **construct_data)
722
856
  )
723
857
  else:
724
858
  # Return dict instead of instance
725
859
  result = validated_data
726
860
 
727
- # Step 7: Run post_load hooks ONLY if they exist (PERFORMANCE OPTIMIZATION)
728
- if postprocess and self._hooks.get("post_load"):
861
+ # Step 7: Run post_load hooks
862
+ if postprocess:
863
+ # MA 4.x requires 'unknown' parameter, MA 3.x doesn't accept it
864
+ post_load_kwargs: dict[str, Any] = {
865
+ "many": False,
866
+ "original_data": data,
867
+ "partial": partial,
868
+ }
869
+ if _MARSHMALLOW_4_PLUS: # pragma: no cover
870
+ post_load_kwargs["unknown"] = unknown_setting
729
871
  result = self._invoke_load_processors(
730
872
  "post_load",
731
- result,
732
- many=False,
733
- original_data=data,
734
- partial=partial,
873
+ cast(dict[str, Any], result),
874
+ **post_load_kwargs,
735
875
  )
736
876
 
737
877
  return result
@@ -975,8 +1115,9 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
975
1115
  fields_to_exclude.add(field_name)
976
1116
 
977
1117
  # Extract computed field values BEFORE converting to dict
978
- if include_computed and hasattr(model_class, 'model_computed_fields'):
979
- for field_name in model_class.model_computed_fields:
1118
+ computed_fields = _get_computed_fields(model_class)
1119
+ if include_computed and computed_fields:
1120
+ for field_name in computed_fields:
980
1121
  value = getattr(obj, field_name)
981
1122
  # Apply exclusion rules to computed fields too
982
1123
  if exclude_none and value is None:
@@ -989,7 +1130,7 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
989
1130
  obj = obj.model_dump(by_alias=False)
990
1131
 
991
1132
  # Let Marshmallow handle the standard dump
992
- result: dict[str, Any] = super().dump(obj, many=False)
1133
+ result: dict[str, Any] = cast(dict[str, Any], super().dump(obj, many=False))
993
1134
 
994
1135
  # Apply field exclusions
995
1136
  if fields_to_exclude:
@@ -1032,6 +1173,20 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
1032
1173
  Returns:
1033
1174
  A PydanticSchema subclass
1034
1175
  """
1176
+ # Build cache key from model, schema_name, and options
1177
+ # Sort options to ensure consistent keys
1178
+ # Note: schema_name must be included since it affects the generated class name
1179
+ cache_key: tuple[type[Any], str | None, tuple[tuple[str, Any], ...]] | None = None
1180
+ try:
1181
+ cache_key = (model, schema_name, tuple(sorted(meta_options.items())))
1182
+ # Thread-safe read (atomic dict lookup)
1183
+ cached = _schema_class_cache.get(cache_key)
1184
+ if cached is not None:
1185
+ return cached
1186
+ except TypeError:
1187
+ # Unhashable options (e.g., list values) - skip cache
1188
+ cache_key = None
1189
+
1035
1190
  name = schema_name or f"{model.__name__}Schema"
1036
1191
 
1037
1192
  # Extract field filtering options
@@ -1063,9 +1218,75 @@ class PydanticSchema(Schema, Generic[M], metaclass=PydanticSchemaMeta):
1063
1218
  class_dict: dict[str, Any] = {"Meta": Meta, **fields}
1064
1219
 
1065
1220
  schema_cls = type(name, (cls,), class_dict)
1221
+
1222
+ # Thread-safe cache write (supports free-threaded Python 3.14+)
1223
+ if cache_key is not None:
1224
+ with _cache_lock:
1225
+ _schema_class_cache[cache_key] = schema_cls
1226
+
1066
1227
  return schema_cls
1067
1228
 
1068
1229
 
1230
+ if _MARSHMALLOW_4_PLUS: # pragma: no cover
1231
+ class PydanticSchema(_PydanticSchema[M], Generic[M]):
1232
+ def __init__(
1233
+ self,
1234
+ *,
1235
+ only: Sequence[str] | None = None,
1236
+ exclude: Sequence[str] = (),
1237
+ load_only: Sequence[str] = (),
1238
+ dump_only: Sequence[str] = (),
1239
+ partial: bool | Sequence[str] | AbstractSet[str] | None = None,
1240
+ unknown: str | None = None,
1241
+ many: bool | None = None,
1242
+ **kwargs: Any,
1243
+ ) -> None:
1244
+ # MA 4.x removed context parameter - reject it to match native behavior
1245
+ if "context" in kwargs:
1246
+ raise TypeError(
1247
+ "PydanticSchema.__init__() got an unexpected keyword argument 'context'. "
1248
+ "The context parameter was removed in Marshmallow 4.x. "
1249
+ "Use contextvars.ContextVar instead."
1250
+ )
1251
+ super().__init__(
1252
+ only=only,
1253
+ exclude=exclude,
1254
+ load_only=load_only,
1255
+ dump_only=dump_only,
1256
+ partial=partial,
1257
+ unknown=unknown,
1258
+ many=many,
1259
+ **kwargs,
1260
+ )
1261
+ else:
1262
+ class PydanticSchema(_PydanticSchema[M], Generic[M]): # type: ignore[no-redef]
1263
+ def __init__(
1264
+ self,
1265
+ *,
1266
+ only: Sequence[str] | None = None,
1267
+ exclude: Sequence[str] = (),
1268
+ context: dict[str, Any] | None = None,
1269
+ load_only: Sequence[str] = (),
1270
+ dump_only: Sequence[str] = (),
1271
+ partial: bool | Sequence[str] | AbstractSet[str] | None = None,
1272
+ unknown: str | None = None,
1273
+ many: bool | None = None,
1274
+ **kwargs: Any,
1275
+ ) -> None:
1276
+ # Pass context to super so Marshmallow sets self.context correctly
1277
+ super().__init__(
1278
+ only=only,
1279
+ exclude=exclude,
1280
+ context=context,
1281
+ load_only=load_only,
1282
+ dump_only=dump_only,
1283
+ partial=partial,
1284
+ unknown=unknown,
1285
+ many=many,
1286
+ **kwargs,
1287
+ )
1288
+
1289
+
1069
1290
  def schema_for(model: type[M], **meta_options: Any) -> type[PydanticSchema[M]]:
1070
1291
  """
1071
1292
  Shortcut to create a Marshmallow schema from a Pydantic model.
@@ -1155,10 +1376,17 @@ class HybridModel(BaseModel):
1155
1376
 
1156
1377
  @classmethod
1157
1378
  def marshmallow_schema(cls) -> type[PydanticSchema[Any]]:
1158
- """Get or create the Marshmallow schema for this model."""
1159
- if cls not in _hybrid_schema_cache:
1160
- _hybrid_schema_cache[cls] = PydanticSchema.from_model(cls)
1161
- return _hybrid_schema_cache[cls]
1379
+ """Get or create the Marshmallow schema for this model (thread-safe)."""
1380
+ # Thread-safe read
1381
+ cached = _hybrid_schema_cache.get(cls)
1382
+ if cached is not None:
1383
+ return cached
1384
+ # Thread-safe write
1385
+ with _cache_lock:
1386
+ # Double-check after acquiring lock
1387
+ if cls not in _hybrid_schema_cache:
1388
+ _hybrid_schema_cache[cls] = PydanticSchema.from_model(cls)
1389
+ return _hybrid_schema_cache[cls]
1162
1390
 
1163
1391
  @classmethod
1164
1392
  def ma_load(cls, data: dict[str, Any], **kwargs: Any) -> HybridModel:
@@ -10,17 +10,37 @@ from __future__ import annotations
10
10
  from types import UnionType
11
11
  from typing import Any, Union, get_args, get_origin
12
12
 
13
- from marshmallow import fields as ma_fields
14
13
  from pydantic import BaseModel
15
14
  from pydantic_core import PydanticUndefined
16
15
 
17
16
  from .type_mapping import type_to_marshmallow_field
18
17
 
19
18
 
19
+ def _get_computed_fields(model_class: type[BaseModel]) -> dict[str, Any]:
20
+ """Get computed fields dict from a Pydantic model class.
21
+
22
+ Handles differences between Pydantic versions:
23
+ - Pydantic 2.0.x: model_computed_fields is a property (only works on instances)
24
+ - Pydantic 2.4+: model_computed_fields works directly on classes
25
+ """
26
+ # Try direct access first (works in newer Pydantic)
27
+ result = getattr(model_class, 'model_computed_fields', None)
28
+ if isinstance(result, dict):
29
+ return result
30
+
31
+ # For Pydantic 2.0.x: model_computed_fields is a property, call its getter
32
+ if isinstance(result, property) and result.fget:
33
+ prop_result = result.fget(model_class)
34
+ if isinstance(prop_result, dict):
35
+ return prop_result
36
+
37
+ return {}
38
+
39
+
20
40
  def convert_pydantic_field(
21
41
  field_name: str,
22
42
  field_info: Any,
23
- ) -> ma_fields.Field:
43
+ ) -> Any:
24
44
  """
25
45
  Convert a single Pydantic FieldInfo to a Marshmallow Field.
26
46
 
@@ -70,7 +90,7 @@ def convert_pydantic_field(
70
90
  def convert_computed_field(
71
91
  field_name: str,
72
92
  computed_info: Any,
73
- ) -> ma_fields.Field:
93
+ ) -> Any:
74
94
  """
75
95
  Convert a Pydantic computed_field to a dump-only Marshmallow Field.
76
96
 
@@ -93,7 +113,7 @@ def convert_model_fields(
93
113
  include: set[str] | None = None,
94
114
  exclude: set[str] | None = None,
95
115
  include_computed: bool = True,
96
- ) -> dict[str, ma_fields.Field]:
116
+ ) -> dict[str, Any]:
97
117
  """
98
118
  Convert all fields from a Pydantic model to Marshmallow fields.
99
119
 
@@ -111,7 +131,7 @@ def convert_model_fields(
111
131
  Returns:
112
132
  Dict mapping field names to Marshmallow field instances
113
133
  """
114
- fields: dict[str, ma_fields.Field] = {}
134
+ fields: dict[str, Any] = {}
115
135
  exclude_set = exclude or set()
116
136
 
117
137
  # Convert regular model fields
@@ -125,8 +145,9 @@ def convert_model_fields(
125
145
  fields[field_name] = convert_pydantic_field(field_name, field_info)
126
146
 
127
147
  # Convert computed fields (dump-only)
128
- if include_computed and hasattr(model, 'model_computed_fields'):
129
- for field_name, computed_info in model.model_computed_fields.items():
148
+ computed_fields = _get_computed_fields(model)
149
+ if include_computed and computed_fields:
150
+ for field_name, computed_info in computed_fields.items():
130
151
  if field_name in exclude_set:
131
152
  continue
132
153
  if include is not None and field_name not in include:
@@ -4,9 +4,13 @@ Type mapping utilities for converting Python/Pydantic types to Marshmallow field
4
4
  This module provides type-to-field conversions, leveraging Marshmallow's native
5
5
  TYPE_MAPPING for basic types and adding support for generic collections.
6
6
  """
7
+
8
+ from __future__ import annotations
9
+
7
10
  from enum import Enum as PyEnum
11
+ from functools import lru_cache
8
12
  from types import UnionType
9
- from typing import Any, Literal, Union, get_args, get_origin
13
+ from typing import Any, Literal, Union, cast, get_args, get_origin
10
14
 
11
15
  from marshmallow import Schema, fields as ma_fields
12
16
  from pydantic import BaseModel
@@ -15,7 +19,21 @@ from pydantic import BaseModel
15
19
  _processing_models: set[type[Any]] = set()
16
20
 
17
21
 
18
- def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
22
+ # Cache for simple type -> field class lookups (str, int, datetime, etc.)
23
+ # These are the most common types and benefit most from caching.
24
+ # maxsize=512 handles large codebases with ~80KB memory overhead.
25
+ @lru_cache(maxsize=512)
26
+ def _get_simple_field_class(type_hint: type) -> type[Any]:
27
+ """
28
+ Cached lookup for simple, hashable types in Marshmallow's TYPE_MAPPING.
29
+
30
+ This avoids repeated dict lookups and isinstance checks for common types
31
+ like str, int, float, bool, datetime, etc.
32
+ """
33
+ return Schema.TYPE_MAPPING.get(type_hint, ma_fields.Raw)
34
+
35
+
36
+ def type_to_marshmallow_field(type_hint: Any) -> Any:
19
37
  """
20
38
  Map a Python type to a Marshmallow field instance.
21
39
 
@@ -36,6 +54,18 @@ def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
36
54
  Returns:
37
55
  An appropriate Marshmallow field instance
38
56
  """
57
+ # FAST PATH: Simple types (str, int, float, bool, datetime, etc.)
58
+ # Check TYPE_MAPPING first to skip all the isinstance/origin checks.
59
+ # This handles ~60-80% of fields in typical models, but we must not
60
+ # short-circuit for Enums or Pydantic models, which have specialized handling.
61
+ if (
62
+ isinstance(type_hint, type)
63
+ and type_hint in Schema.TYPE_MAPPING
64
+ and not issubclass(type_hint, (PyEnum, BaseModel))
65
+ ):
66
+ # Cast needed for mypy: type_hint is confirmed to be a type at this point
67
+ return _get_simple_field_class(cast(type, type_hint))()
68
+
39
69
  origin = get_origin(type_hint)
40
70
  args = get_args(type_hint)
41
71
 
@@ -61,40 +91,47 @@ def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
61
91
  return ma_fields.Raw(allow_none=True)
62
92
 
63
93
  # Handle Enum types
64
- if isinstance(type_hint, type) and issubclass(type_hint, PyEnum):
65
- return ma_fields.Enum(type_hint)
94
+ # Use try-except because some type hints pass isinstance(x, type) but fail issubclass()
95
+ try:
96
+ if isinstance(type_hint, type) and issubclass(type_hint, PyEnum):
97
+ return ma_fields.Enum(type_hint)
98
+ except TypeError:
99
+ pass # Not a valid class for issubclass check
66
100
 
67
101
  # Handle nested Pydantic models - use Nested with a dynamically created schema
68
- if isinstance(type_hint, type) and issubclass(type_hint, BaseModel):
69
- # Import here to avoid circular imports
70
- from pydantic_marshmallow.bridge import PydanticSchema
71
-
72
- # Check if we're already processing this model (recursion detection)
73
- # Use a module-level set to track models being processed
74
- if type_hint in _processing_models:
75
- # Self-referential model - use Raw to avoid infinite recursion
76
- # Pydantic will still handle the validation correctly
77
- return ma_fields.Raw()
78
-
79
- try:
80
- _processing_models.add(type_hint)
81
- # Create a nested schema class for this model
82
- nested_schema = PydanticSchema.from_model(type_hint)
83
- return ma_fields.Nested(nested_schema)
84
- finally:
85
- _processing_models.discard(type_hint)
102
+ try:
103
+ if isinstance(type_hint, type) and issubclass(type_hint, BaseModel):
104
+ # Import here to avoid circular imports
105
+ from pydantic_marshmallow.bridge import PydanticSchema
106
+
107
+ # Check if we're already processing this model (recursion detection)
108
+ # Use a module-level set to track models being processed
109
+ if type_hint in _processing_models:
110
+ # Self-referential model - use Raw to avoid infinite recursion
111
+ # Pydantic will still handle the validation correctly
112
+ return ma_fields.Raw()
113
+
114
+ try:
115
+ _processing_models.add(type_hint)
116
+ # Create a nested schema class for this model
117
+ nested_schema = PydanticSchema.from_model(type_hint)
118
+ return ma_fields.Nested(nested_schema)
119
+ finally:
120
+ _processing_models.discard(type_hint)
121
+ except TypeError:
122
+ pass # Not a valid class for issubclass check
86
123
 
87
124
  # Handle list[T]
88
125
  if origin is list:
89
- inner: ma_fields.Field = ma_fields.Raw()
126
+ inner = ma_fields.Raw()
90
127
  if args:
91
128
  inner = type_to_marshmallow_field(args[0])
92
129
  return ma_fields.List(inner)
93
130
 
94
131
  # Handle dict[K, V]
95
132
  if origin is dict:
96
- key_field: ma_fields.Field = ma_fields.String()
97
- value_field: ma_fields.Field = ma_fields.Raw()
133
+ key_field = ma_fields.String()
134
+ value_field = ma_fields.Raw()
98
135
  if args and len(args) >= 2:
99
136
  key_field = type_to_marshmallow_field(args[0])
100
137
  value_field = type_to_marshmallow_field(args[1])
@@ -102,7 +139,7 @@ def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
102
139
 
103
140
  # Handle set[T] and frozenset[T] - convert to List in Marshmallow
104
141
  if origin in (set, frozenset):
105
- inner_set: ma_fields.Field = ma_fields.Raw()
142
+ inner_set = ma_fields.Raw()
106
143
  if args:
107
144
  inner_set = type_to_marshmallow_field(args[0])
108
145
  return ma_fields.List(inner_set)
@@ -128,11 +165,11 @@ def type_to_marshmallow_field(type_hint: Any) -> ma_fields.Field:
128
165
  if any(ut in type_name for ut in url_types):
129
166
  return ma_fields.URL()
130
167
  if 'IP' in type_name:
131
- # IP field exists in marshmallow but may not have type stubs
132
- return ma_fields.IP() # type: ignore[no-untyped-call]
168
+ return ma_fields.IP() # type: ignore[no-untyped-call,unused-ignore]
133
169
 
134
170
  # Use Marshmallow's native TYPE_MAPPING for basic types
135
171
  # This ensures we stay in sync with Marshmallow's type handling
136
172
  resolved = origin if origin else type_hint
137
- field_cls = Schema.TYPE_MAPPING.get(resolved, ma_fields.Raw)
138
- return field_cls()
173
+ if isinstance(resolved, type):
174
+ return _get_simple_field_class(resolved)()
175
+ return ma_fields.Raw()
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-marshmallow
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Bring Pydantic's power to Marshmallow: type annotations, validators, and automatic schema generation
5
- Author-email: Your Name <your.email@example.com>
5
+ Author: Michael Thomas
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/mockodin/pydantic-marshmallow
8
8
  Project-URL: Documentation, https://mockodin.github.io/pydantic-marshmallow
@@ -24,18 +24,17 @@ Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.10
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
- Requires-Dist: marshmallow>=3.18.0
28
- Requires-Dist: pydantic>=2.0.0
27
+ Requires-Dist: marshmallow<5.0.0,>=3.18.0
28
+ Requires-Dist: pydantic<3.0.0,>=2.0.0
29
29
  Requires-Dist: typing-extensions>=4.0.0
30
30
  Requires-Dist: orjson>=3.9.0
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: pytest>=8.0.0; extra == "dev"
33
33
  Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
34
34
  Requires-Dist: email-validator>=2.1.0; extra == "dev"
35
- Requires-Dist: mypy>=1.10.0; extra == "dev"
36
- Requires-Dist: ruff>=0.4.0; extra == "dev"
37
- Requires-Dist: flake8>=7.1.0; extra == "dev"
38
- Requires-Dist: flake8-pyproject>=1.2.4; extra == "dev"
35
+ Requires-Dist: mypy==1.19.1; extra == "dev"
36
+ Requires-Dist: ruff==0.15.0; extra == "dev"
37
+ Requires-Dist: pre-commit>=4.2.0; extra == "dev"
39
38
  Requires-Dist: sqlalchemy>=2.0.30; extra == "dev"
40
39
  Requires-Dist: marshmallow-sqlalchemy>=1.1.0; extra == "dev"
41
40
  Requires-Dist: flask>=3.0.0; extra == "dev"
@@ -67,6 +66,40 @@ Bridge Pydantic's power with Marshmallow's ecosystem. Use Pydantic models for va
67
66
 
68
67
  📖 **[Documentation](https://mockodin.github.io/pydantic-marshmallow)** | 🐙 **[GitHub](https://github.com/mockodin/pydantic-marshmallow)**
69
68
 
69
+ ## Why pydantic-marshmallow?
70
+
71
+ Get the best of both worlds: **Pydantic's speed** with **Marshmallow's ecosystem**.
72
+
73
+ ### Performance
74
+
75
+ pydantic-marshmallow uses Pydantic's Rust-powered validation engine under the hood, delivering significant performance improvements over native Marshmallow—especially for nested data structures.
76
+
77
+ #### Performance Comparison
78
+
79
+ | Operation | MA 3.x | MA 4.x | Bridge (MA 3.x) | Bridge (MA 4.x) |
80
+ |-----------|--------|--------|-----------------|-----------------|
81
+ | Simple load | 5.3 µs | 4.8 µs | 2.9 µs | 2.9 µs |
82
+ | Nested model | 10.9 µs | 10.5 µs | 3.4 µs | 3.2 µs |
83
+ | Deep nested (4 levels) | 31.2 µs | 29.0 µs | 5.3 µs | 5.2 µs |
84
+ | Batch (100 items) | 454 µs | 424 µs | 260 µs | 259 µs |
85
+
86
+ #### What the Numbers Show
87
+
88
+ 1. **MA 3.x → MA 4.x**: Marshmallow 4.x improved ~10% over 3.x (5.3 → 4.8 µs for simple loads)
89
+ 2. **MA 3.x → Bridge**: Adding Pydantic delivers **1.8x–5.9x speedup** over pure MA 3.x
90
+ 3. **MA 4.x → Bridge**: Still **1.6x–5.6x faster** than native MA 4.x
91
+ 4. **Bridge consistency**: ~2.9 µs for simple loads regardless of Marshmallow version
92
+
93
+ The bridge delegates validation to Pydantic's Rust-powered core, bypassing Marshmallow's field processing. This means bridge performance is **independent of Marshmallow version**—you get consistent speed whether you're on MA 3.x or 4.x.
94
+
95
+ *Benchmarks: Python 3.11, median of 3 runs with IQR outlier removal. Run `python -m benchmarks.run_benchmarks` to reproduce.*
96
+
97
+ ### Why it matters
98
+
99
+ - **Existing Marshmallow projects**: Incrementally adopt Pydantic validation without rewriting your API layer
100
+ - **Flask/webargs/apispec users**: Keep your integrations, get faster validation
101
+ - **Performance-sensitive APIs**: Nested model validation is 3-6x faster than native Marshmallow
102
+
70
103
  ## Features
71
104
 
72
105
  - **Pydantic Validation**: Leverage Pydantic's Rust-powered validation engine
@@ -84,6 +117,8 @@ Bridge Pydantic's power with Marshmallow's ecosystem. Use Pydantic models for va
84
117
  pip install pydantic-marshmallow
85
118
  ```
86
119
 
120
+ **Requirements:** Python 3.10+, Pydantic 2.0+, Marshmallow 3.18+ (including 4.x)
121
+
87
122
  ## Quick Start
88
123
 
89
124
  ### Basic Usage
@@ -213,7 +248,7 @@ class User(BaseModel):
213
248
  first: str
214
249
  last: str
215
250
 
216
- @computed_field
251
+ @computed_field # type: ignore[misc]
217
252
  @property
218
253
  def full_name(self) -> str:
219
254
  return f"{self.first} {self.last}"
@@ -237,6 +272,21 @@ schema.dump(user, exclude_unset=True)
237
272
  schema.dump(user, exclude_defaults=True)
238
273
  ```
239
274
 
275
+ ## JSON Serialization
276
+
277
+ Direct JSON string support:
278
+
279
+ ```python
280
+ # Deserialize from JSON string
281
+ user = schema.loads('{"name": "Alice", "email": "alice@example.com", "age": 30}')
282
+
283
+ # Serialize to JSON string
284
+ json_str = schema.dumps(user)
285
+
286
+ # Batch operations
287
+ users = schema.loads('[{"name": "Alice", ...}, {"name": "Bob", ...}]', many=True)
288
+ ```
289
+
240
290
  ## Flask-Marshmallow Integration
241
291
 
242
292
  ```python
@@ -288,6 +338,99 @@ spec = APISpec(
288
338
  spec.components.schema("User", schema=UserSchema)
289
339
  ```
290
340
 
341
+ ## flask-smorest Integration
342
+
343
+ Build REST APIs with automatic OpenAPI documentation:
344
+
345
+ ```python
346
+ from flask import Flask
347
+ from flask_smorest import Api, Blueprint
348
+ from pydantic import BaseModel, Field
349
+ from pydantic_marshmallow import schema_for
350
+
351
+ app = Flask(__name__)
352
+ app.config["API_TITLE"] = "My API"
353
+ app.config["API_VERSION"] = "v1"
354
+ app.config["OPENAPI_VERSION"] = "3.0.2"
355
+
356
+ api = Api(app)
357
+ blp = Blueprint("users", __name__, url_prefix="/users")
358
+
359
+ class UserCreate(BaseModel):
360
+ name: str = Field(min_length=1)
361
+ email: str
362
+
363
+ UserCreateSchema = schema_for(UserCreate)
364
+ UserSchema = schema_for(User)
365
+
366
+ @blp.post("/")
367
+ @blp.arguments(UserCreateSchema)
368
+ @blp.response(201, UserSchema)
369
+ def create_user(data):
370
+ # data is a Pydantic UserCreate instance
371
+ user = User(id=1, name=data.name, email=data.email)
372
+ return UserSchema().dump(user)
373
+
374
+ api.register_blueprint(blp)
375
+ ```
376
+
377
+ ## flask-rebar Integration
378
+
379
+ Build REST APIs with automatic Swagger documentation:
380
+
381
+ ```python
382
+ from flask import Flask
383
+ from flask_rebar import Rebar, get_validated_body
384
+ from pydantic_marshmallow import schema_for
385
+
386
+ app = Flask(__name__)
387
+ rebar = Rebar()
388
+ registry = rebar.create_handler_registry()
389
+
390
+ UserCreateSchema = schema_for(UserCreate)
391
+ UserSchema = schema_for(User)
392
+
393
+ @registry.handles(
394
+ rule="/users",
395
+ method="POST",
396
+ request_body_schema=UserCreateSchema(),
397
+ response_body_schema=UserSchema(),
398
+ )
399
+ def create_user():
400
+ data = get_validated_body() # Pydantic UserCreate instance
401
+ user = User(id=1, name=data.name, email=data.email)
402
+ return UserSchema().dump(user)
403
+
404
+ rebar.init_app(app)
405
+ ```
406
+
407
+ ## SQLAlchemy Pattern
408
+
409
+ Use Pydantic for API validation alongside SQLAlchemy ORM:
410
+
411
+ ```python
412
+ from sqlalchemy.orm import Session
413
+ from pydantic_marshmallow import schema_for
414
+
415
+ # Pydantic model for API validation
416
+ class UserCreate(BaseModel):
417
+ name: str = Field(min_length=1)
418
+ email: str
419
+
420
+ UserCreateSchema = schema_for(UserCreate)
421
+
422
+ def create_user(session: Session, data: dict):
423
+ # Validate API input with Pydantic
424
+ schema = UserCreateSchema()
425
+ validated = schema.load(data) # Returns Pydantic model
426
+
427
+ # Create ORM object from validated data
428
+ orm_user = UserModel(name=validated.name, email=validated.email)
429
+ session.add(orm_user)
430
+ session.commit()
431
+ return orm_user
432
+ ```
433
+
291
434
  ## HybridModel
292
435
 
293
436
  For models that need both Pydantic and Marshmallow APIs:
@@ -0,0 +1,12 @@
1
+ pydantic_marshmallow/__init__.py,sha256=EQON4HpiOFDtC33MrrE6WFW68GBtDlx1IqbDWxbqghU,3100
2
+ pydantic_marshmallow/bridge.py,sha256=fBk9vv6xZd-r-rhHufl-fbWSt784SEQ-IYrlm3duzpY,58026
3
+ pydantic_marshmallow/errors.py,sha256=slWs9m6Iid32ZUg8uxzNbFoZWFqh7ZuiG5qPlcOaBuc,5304
4
+ pydantic_marshmallow/field_conversion.py,sha256=WaWhvpB8rLL05I_D8b9q7BCymf24L7rxOpjMJY-TtYo,4978
5
+ pydantic_marshmallow/py.typed,sha256=GehYepOFQ5fvmwg3UbxoFj6w91p2kWD3KUBXL_rJRUI,86
6
+ pydantic_marshmallow/type_mapping.py,sha256=XlgOH3kksrv2kratQ1B1J1AJZj2spu7izoIF58hZeJs,6751
7
+ pydantic_marshmallow/validators.py,sha256=hg7ZTrjGHcyzfOtGy6T1LLGx4puRD3VSCLf0KN0PE8g,6686
8
+ pydantic_marshmallow-1.1.0.dist-info/licenses/LICENSE,sha256=y1B14nDO7pR7JdBNaWTLDt9MPBVRVuKZTUiRkw_eVgg,1071
9
+ pydantic_marshmallow-1.1.0.dist-info/METADATA,sha256=gwysXAIX0iAhRfpAe1aCzQPQEX_u5DC-YYNcMu1zghg,15021
10
+ pydantic_marshmallow-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ pydantic_marshmallow-1.1.0.dist-info/top_level.txt,sha256=sQCtMe_dpBvCrrBM6YIz1UQmGSf-XjdoBUeA2uyuoh4,21
12
+ pydantic_marshmallow-1.1.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- pydantic_marshmallow/__init__.py,sha256=IeZH-ZcLxyy_OMfIn-IxbPRxskOnPxY94E7FLJEEBcA,2831
2
- pydantic_marshmallow/bridge.py,sha256=JkDF-BozWZtzTl31RMqRHPEV_eKbeybnsuEPNdrU--Q,47688
3
- pydantic_marshmallow/errors.py,sha256=slWs9m6Iid32ZUg8uxzNbFoZWFqh7ZuiG5qPlcOaBuc,5304
4
- pydantic_marshmallow/field_conversion.py,sha256=xFQtR63DwTDq-hGA_8Xe8sO6lFW6BCZNmnk5oxz5B6w,4262
5
- pydantic_marshmallow/py.typed,sha256=GehYepOFQ5fvmwg3UbxoFj6w91p2kWD3KUBXL_rJRUI,86
6
- pydantic_marshmallow/type_mapping.py,sha256=4gFVZV7WVywyugTyiE-y7saWaHmcaPeCs0vQjBQBQ8I,5285
7
- pydantic_marshmallow/validators.py,sha256=hg7ZTrjGHcyzfOtGy6T1LLGx4puRD3VSCLf0KN0PE8g,6686
8
- pydantic_marshmallow-1.0.0.dist-info/licenses/LICENSE,sha256=y1B14nDO7pR7JdBNaWTLDt9MPBVRVuKZTUiRkw_eVgg,1071
9
- pydantic_marshmallow-1.0.0.dist-info/METADATA,sha256=gVyLe10AtxyW4hJ9NTe43gIyPPBwigeu_BLWyopYfdg,10556
10
- pydantic_marshmallow-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
- pydantic_marshmallow-1.0.0.dist-info/top_level.txt,sha256=sQCtMe_dpBvCrrBM6YIz1UQmGSf-XjdoBUeA2uyuoh4,21
12
- pydantic_marshmallow-1.0.0.dist-info/RECORD,,