0
# Dynamic Fields
1
2
Advanced field functionality for computed properties, serializable fields, and dynamic field generation in Schematics. These features enable flexible data modeling with calculated values, custom serialization, and runtime field creation.
3
4
## Capabilities
5
6
### Serializable Fields
7
8
Create computed fields that appear in model serialization with custom calculation logic.
9
10
```python { .api }
11
def serializable(func=None, **field_kwargs):
12
"""
13
Decorator for creating serializable computed fields.
14
15
Creates fields that compute their values dynamically and include
16
the results in model export and serialization operations.
17
18
Args:
19
func (callable, optional): Function to compute field value
20
**field_kwargs: Additional field options (type, required, etc.)
21
22
Returns:
23
Serializable field descriptor
24
"""
25
26
class Serializable:
27
"""
28
Dynamic serializable field descriptor for computed properties.
29
30
Enables creation of fields that compute values at export time
31
based on other model data or external calculations.
32
"""
33
34
def __init__(self, **kwargs):
35
"""
36
Initialize serializable field.
37
38
Args:
39
**kwargs: Field configuration options
40
"""
41
```
42
43
### Calculated Fields
44
45
Create fields with values calculated from other model fields.
46
47
```python { .api }
48
def calculated(func, **field_kwargs):
49
"""
50
Create calculated fields based on other model data.
51
52
Calculated fields automatically compute their values when accessed
53
based on the current state of other model fields.
54
55
Args:
56
func (callable): Function to calculate field value
57
**field_kwargs: Additional field options
58
59
Returns:
60
Calculated field descriptor
61
"""
62
```
63
64
### Dynamic Field Creation
65
66
Runtime field creation and model customization capabilities.
67
68
```python { .api }
69
class DynamicModel(Model):
70
"""
71
Model subclass supporting runtime field addition.
72
73
Enables dynamic model schemas that can be modified at runtime
74
based on configuration or external requirements.
75
"""
76
77
@classmethod
78
def add_field(cls, name, field_type):
79
"""
80
Add field to model class at runtime.
81
82
Args:
83
name (str): Field name
84
field_type (BaseType): Field type instance
85
"""
86
```
87
88
## Usage Examples
89
90
### Basic Serializable Fields
91
92
```python
93
from schematics.models import Model
94
from schematics.types import StringType, IntType, DateTimeType, serializable
95
from datetime import datetime
96
97
class User(Model):
98
first_name = StringType(required=True)
99
last_name = StringType(required=True)
100
birth_year = IntType()
101
created_at = DateTimeType(default=datetime.utcnow)
102
103
@serializable
104
def full_name(self):
105
"""Computed full name from first and last name."""
106
return f"{self.first_name} {self.last_name}"
107
108
@serializable
109
def age(self):
110
"""Calculated age from birth year."""
111
if self.birth_year:
112
return datetime.now().year - self.birth_year
113
return None
114
115
@serializable
116
def account_age_days(self):
117
"""Days since account creation."""
118
if self.created_at:
119
return (datetime.utcnow() - self.created_at).days
120
return 0
121
122
# Usage
123
user = User({
124
'first_name': 'John',
125
'last_name': 'Doe',
126
'birth_year': 1990
127
})
128
129
# Computed fields available in serialization
130
user_data = user.to_primitive()
131
print(user_data)
132
# {
133
# 'first_name': 'John',
134
# 'last_name': 'Doe',
135
# 'birth_year': 1990,
136
# 'created_at': '2024-01-15T10:30:00.123456Z',
137
# 'full_name': 'John Doe',
138
# 'age': 34,
139
# 'account_age_days': 0
140
# }
141
```
142
143
### Advanced Serializable Fields with Type Hints
144
145
```python
146
from schematics.types import FloatType, ListType, StringType
147
148
class Product(Model):
149
name = StringType(required=True)
150
base_price = FloatType(required=True, min_value=0)
151
tax_rate = FloatType(default=0.08, min_value=0, max_value=1)
152
discount_percent = FloatType(default=0, min_value=0, max_value=100)
153
tags = ListType(StringType())
154
155
@serializable(type=FloatType())
156
def tax_amount(self):
157
"""Calculate tax amount."""
158
return self.base_price * self.tax_rate
159
160
@serializable(type=FloatType())
161
def discount_amount(self):
162
"""Calculate discount amount."""
163
return self.base_price * (self.discount_percent / 100)
164
165
@serializable(type=FloatType())
166
def final_price(self):
167
"""Calculate final price after tax and discount."""
168
return self.base_price + self.tax_amount - self.discount_amount
169
170
@serializable(type=StringType())
171
def tag_summary(self):
172
"""Create summary of product tags."""
173
if self.tags:
174
return ', '.join(self.tags)
175
return 'No tags'
176
177
@serializable(type=StringType())
178
def price_tier(self):
179
"""Determine price tier based on final price."""
180
if self.final_price < 20:
181
return 'Budget'
182
elif self.final_price < 100:
183
return 'Standard'
184
else:
185
return 'Premium'
186
187
# Usage
188
product = Product({
189
'name': 'Wireless Headphones',
190
'base_price': 99.99,
191
'tax_rate': 0.08,
192
'discount_percent': 15,
193
'tags': ['electronics', 'audio', 'wireless']
194
})
195
196
print(product.to_primitive())
197
# Includes all computed fields: tax_amount, discount_amount, final_price, etc.
198
```
199
200
### Calculated Fields for Data Aggregation
201
202
```python
203
from schematics.types import ModelType, ListType, calculated
204
205
class OrderItem(Model):
206
product_name = StringType(required=True)
207
quantity = IntType(required=True, min_value=1)
208
unit_price = FloatType(required=True, min_value=0)
209
210
@serializable(type=FloatType())
211
def line_total(self):
212
return self.quantity * self.unit_price
213
214
class Order(Model):
215
order_id = StringType(required=True)
216
customer_name = StringType(required=True)
217
items = ListType(ModelType(OrderItem), required=True, min_size=1)
218
shipping_cost = FloatType(default=0, min_value=0)
219
220
@calculated(type=FloatType())
221
def subtotal(self):
222
"""Calculate subtotal from all line items."""
223
return sum(item.line_total for item in self.items)
224
225
@calculated(type=IntType())
226
def total_items(self):
227
"""Count total quantity across all items."""
228
return sum(item.quantity for item in self.items)
229
230
@calculated(type=FloatType())
231
def total_amount(self):
232
"""Calculate final total including shipping."""
233
return self.subtotal + self.shipping_cost
234
235
@serializable(type=StringType())
236
def order_summary(self):
237
"""Generate human-readable order summary."""
238
return f"Order {self.order_id}: {self.total_items} items, ${self.total_amount:.2f}"
239
240
# Usage
241
order = Order({
242
'order_id': 'ORD-001',
243
'customer_name': 'Jane Smith',
244
'shipping_cost': 5.99,
245
'items': [
246
{'product_name': 'Widget A', 'quantity': 2, 'unit_price': 12.50},
247
{'product_name': 'Widget B', 'quantity': 1, 'unit_price': 25.00}
248
]
249
})
250
251
print(f"Subtotal: ${order.subtotal:.2f}") # $50.00
252
print(f"Total items: {order.total_items}") # 3
253
print(f"Final total: ${order.total_amount:.2f}") # $55.99
254
print(order.order_summary) # "Order ORD-001: 3 items, $55.99"
255
```
256
257
### Dynamic Model Creation
258
259
```python
260
def create_survey_model(questions):
261
"""
262
Dynamically create a survey model based on question configuration.
263
264
Args:
265
questions (list): List of question dictionaries
266
267
Returns:
268
Model: Dynamically created survey model class
269
"""
270
271
class SurveyResponse(Model):
272
respondent_id = StringType(required=True)
273
submitted_at = DateTimeType(default=datetime.utcnow)
274
275
@serializable(type=IntType())
276
def response_count(self):
277
"""Count non-empty responses."""
278
count = 0
279
for field_name, field in self._fields.items():
280
if field_name not in ['respondent_id', 'submitted_at']:
281
value = getattr(self, field_name, None)
282
if value is not None and value != '':
283
count += 1
284
return count
285
286
# Add dynamic fields based on questions
287
for question in questions:
288
field_name = question['name']
289
field_type = question['type']
290
291
if field_type == 'text':
292
field = StringType(required=question.get('required', False))
293
elif field_type == 'number':
294
field = IntType(required=question.get('required', False))
295
elif field_type == 'choice':
296
field = StringType(
297
required=question.get('required', False),
298
choices=question.get('choices', [])
299
)
300
elif field_type == 'multiple_choice':
301
field = ListType(StringType(choices=question.get('choices', [])))
302
else:
303
field = StringType()
304
305
# Add field to model class
306
setattr(SurveyResponse, field_name, field)
307
308
return SurveyResponse
309
310
# Define survey questions
311
survey_questions = [
312
{'name': 'satisfaction', 'type': 'choice', 'required': True,
313
'choices': ['very_satisfied', 'satisfied', 'neutral', 'dissatisfied']},
314
{'name': 'recommendation_score', 'type': 'number', 'required': True},
315
{'name': 'feedback', 'type': 'text', 'required': False},
316
{'name': 'interests', 'type': 'multiple_choice',
317
'choices': ['tech', 'sports', 'music', 'travel']}
318
]
319
320
# Create dynamic survey model
321
SurveyModel = create_survey_model(survey_questions)
322
323
# Use the dynamically created model
324
response = SurveyModel({
325
'respondent_id': 'RESP-001',
326
'satisfaction': 'satisfied',
327
'recommendation_score': 8,
328
'feedback': 'Great service!',
329
'interests': ['tech', 'music']
330
})
331
332
response.validate()
333
print(f"Response count: {response.response_count}") # 4
334
print(response.to_primitive())
335
```
336
337
### Conditional Serializable Fields
338
339
```python
340
class Report(Model):
341
title = StringType(required=True)
342
data = ListType(IntType(), required=True)
343
include_statistics = BooleanType(default=False)
344
include_chart = BooleanType(default=False)
345
346
@serializable(type=FloatType())
347
def average(self):
348
"""Calculate average of data points."""
349
if self.data:
350
return sum(self.data) / len(self.data)
351
return 0
352
353
@serializable(type=IntType())
354
def data_count(self):
355
"""Count of data points."""
356
return len(self.data) if self.data else 0
357
358
@serializable
359
def statistics(self):
360
"""Include detailed statistics if requested."""
361
if not self.include_statistics:
362
return None
363
364
if not self.data:
365
return None
366
367
data_sorted = sorted(self.data)
368
return {
369
'min': min(data_sorted),
370
'max': max(data_sorted),
371
'median': data_sorted[len(data_sorted) // 2],
372
'range': max(data_sorted) - min(data_sorted)
373
}
374
375
@serializable
376
def chart_config(self):
377
"""Include chart configuration if requested."""
378
if not self.include_chart:
379
return None
380
381
return {
382
'type': 'line',
383
'data': self.data,
384
'title': self.title,
385
'y_axis_label': 'Values'
386
}
387
388
# Usage with different configurations
389
basic_report = Report({
390
'title': 'Sales Data',
391
'data': [100, 150, 120, 180, 200]
392
})
393
394
detailed_report = Report({
395
'title': 'Sales Data',
396
'data': [100, 150, 120, 180, 200],
397
'include_statistics': True,
398
'include_chart': True
399
})
400
401
basic_data = basic_report.to_primitive()
402
# Contains: title, data, average, data_count (statistics and chart_config are None)
403
404
detailed_data = detailed_report.to_primitive()
405
# Contains: all basic fields plus statistics and chart_config objects
406
```