Phony Expression Language (PEL)
PEL is the template syntax used within Phony for composing and transforming generated data. It uses {{expression}} placeholders that are resolved at generation time.
Basic Syntax
┌─────────────────────────────────────────────────────────────────────────┐
│ PEL SYNTAX OVERVIEW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ PLACEHOLDERS: │
│ ────────────── │
│ { {generator_name} } Reference a generator │
│ { {generator.property} } Access nested property │
│ { {function(arg)} } Apply transformation │
│ { {function(gen, arg)} } Transform generator output │
│ │
│ LITERAL TEXT: │
│ ────────────── │
│ Everything outside { {...} } is literal text │
│ │
│ ESCAPING: │
│ ───────── │
│ \{ \{ produces literal { { │
│ \} \} produces literal } } │
│ │
└─────────────────────────────────────────────────────────────────────────┘Note: Template syntax uses double curly braces:
{{ expression }}
Generator References
Basic Reference
Reference any generator defined in the schema:
{
"generators": {
"first_name": {
"type": "model",
"source": "models/names.ngram"
},
"greeting": {
"type": "template",
"pattern": "Hello, {{first_name}}!"
}
}
}Output: "Hello, Mehmet!"
Entity Field Reference
Reference fields from entities (for relationships):
{
"entities": {
"User": {
"fields": {
"id": { "generator": "user_id" },
"name": { "generator": "first_name" }
}
},
"Order": {
"fields": {
"user_id": { "ref": "User.id" }
}
}
}
}Self Reference
Reference other fields in the same entity:
{
"entities": {
"User": {
"fields": {
"first_name": { "generator": "first_name" },
"last_name": { "generator": "last_name" },
"full_name": {
"type": "template",
"pattern": "{{self.first_name}} {{self.last_name}}"
}
}
}
}
}Nested Property Access
Access properties of object-returning generators:
{
"generators": {
"country": {
"type": "list",
"source": "lists/countries.json"
},
"phone": {
"type": "template",
"pattern": "{{country.phone_prefix}} {{pattern:### ### ## ##}}"
}
}
}Output: "+90 532 847 23 91"
Note: List file returns objects like
{ "name": "Turkey", "code": "TR", "phone_prefix": "+90" }
Inline Generators
Generate values directly within templates without separate generator definitions.
Numbers
{{number:1-100}} # Random int 1-100
{{number:0.0-1.0}} # Random float 0.0-1.0
{{number:0.0-1.0:2}} # Random float, 2 decimal places
{{number:1-100:step:5}} # Random from 1,5,10,15...100Patterns
Pattern characters:
#→ Random digit (0-9)?→ Random letter (A-Z)^→ Random hex (0-F)*→ Random alphanumeric
{{pattern:###-##-####}} # 123-45-6789 (SSN format)
{{pattern:??-####}} # AB-1234 (license plate)
{{pattern:^^^^-^^^^-^^^^-^^^^}} # A3F2-B7C1-9D4E-2F8A (hex code)
{{pattern:****-****}} # K7b2-9Xm4 (alphanumeric)Dates and Times
{{date:2020-01-01..2024-12-31}} # Random date in range
{{date:2020-01-01..now}} # From date to now
{{datetime:-1year..now}} # Relative ranges
{{time:09:00..18:00}} # Random time
{{datetime:now-7days..now:ISO8601}} # With formatUUIDs and IDs
{{uuid}} # UUID v4
{{uuid:v7}} # UUID v7
{{ulid}} # ULID
{{nanoid}} # Nano ID (21 chars)
{{nanoid:10}} # Nano ID (10 chars)List Operations
pick()
Random selection from a list:
{{pick(email_domains)}} # Random from generator/list
{{pick(["gmail.com", "yahoo.com", "hotmail.com"])}} # Inline listpick_weighted()
Weighted random selection:
{{pick_weighted(order_status)}} # Uses weights from generator definitioncycle()
Sequential cycling through values:
{{cycle(days_of_week)}} # Monday, Tuesday, ... (repeats)
{{cycle(["A", "B", "C"])}} # A, B, C, A, B, C, ...sample()
Multiple unique selections:
{{sample(tags, 3)}} # Pick 3 unique items
{{sample(categories, 2-5)}} # Pick 2-5 unique itemsString Transformations
Case Transformations
{{lowercase(first_name)}} # mehmet
{{uppercase(city)}} # İSTANBUL
{{capitalize(word)}} # Mehmet
{{titlecase(full_name)}} # Mehmet Yılmaz
{{camelcase(field_name)}} # fieldName
{{snakecase(field_name)}} # field_name
{{kebabcase(field_name)}} # field-name
{{pascalcase(field_name)}} # FieldNameString Operations
{{trim(text)}} # Remove whitespace
{{truncate(description, 50)}} # First 50 chars
{{truncate(desc, 50, "...")}} # With suffix
{{pad(code, 6, "0")}} # 000042
{{padRight(code, 6, "0")}} # 420000
{{reverse(text)}} # Reverse string
{{repeat(char, 5)}} # Repeat 5 timesSlug and URL
{{slugify(product_name)}} # yilmaz-holding-as
{{urlEncode(query)}} # URL-safe encoding
{{base64(data)}} # Base64 encodeData Masking and Privacy
Masking
{{mask(phone, 4)}} # ******4567 (show last 4)
{{mask(phone, 4, "start")}} # 0532****** (show first 4)
{{mask(email, "partial")}} # m***t@g***l.com
{{mask(credit_card, 4, "*")}} # ************1234Hashing
{{hash(email)}} # SHA256 hash
{{hash(email, "md5")}} # MD5 hash
{{hash(email, "sha1")}} # SHA1 hash
{{hmac(data, secret)}} # HMAC-SHA256Anonymization
{{anonymize(name)}} # Consistent fake replacement
{{anonymize(email, "domain")}} # Keep domain, fake local part
{{redact(ssn)}} # [REDACTED]Numeric Operations
Arithmetic
{{add(price, tax)}} # price + tax
{{subtract(total, discount)}} # total - discount
{{multiply(price, quantity)}} # price * quantity
{{divide(total, count)}} # total / count
{{modulo(number, 10)}} # number % 10Formatting
{{format(price, "currency")}} # $1,234.56
{{format(price, "currency:TRY")}} # ₺1.234,56
{{format(number, "decimal:2")}} # 1234.56
{{format(number, "percent")}} # 12.34%
{{format(bytes, "filesize")}} # 1.5 MBMath Functions
{{round(number)}} # Round to integer
{{round(number, 2)}} # Round to 2 decimals
{{floor(number)}} # Round down
{{ceil(number)}} # Round up
{{abs(number)}} # Absolute value
{{min(a, b)}} # Minimum
{{max(a, b)}} # Maximum
{{clamp(value, min, max)}} # Constrain to rangeDate and Time Functions
Current Time
{{now()}} # Current datetime
{{today()}} # Current date
{{now("Y-m-d")}} # FormattedDate Manipulation
{{addDays(date, 7)}} # Add 7 days
{{addMonths(date, 3)}} # Add 3 months
{{addYears(date, 1)}} # Add 1 year
{{subtractDays(date, 7)}} # Subtract 7 days
{{startOfMonth(date)}} # First day of month
{{endOfMonth(date)}} # Last day of month
{{startOfYear(date)}} # January 1Date Formatting
{{format(date, "Y-m-d")}} # 2024-03-15
{{format(date, "d/m/Y")}} # 15/03/2024
{{format(date, "F j, Y")}} # March 15, 2024
{{format(datetime, "ISO8601")}} # 2024-03-15T14:30:00Z
{{format(datetime, "RFC2822")}} # Fri, 15 Mar 2024 14:30:00 +0000Date Calculations
{{dateDiff(start, end, "days")}} # Days between
{{dateDiff(start, end, "months")}}# Months between
{{age(birthdate)}} # Years since dateConditional Logic
if()
{{if(age > 65, "Senior", "Adult")}}
{{if(score >= 90, "A", if(score >= 80, "B", "C"))}} # Nested
{{if(is_premium, premium_price, regular_price)}}switch()
{{switch(status,
active: "Aktif",
pending: "Beklemede",
cancelled: "İptal",
default: "Bilinmiyor"
)}}
{{switch(gender,
M: "Bay",
F: "Bayan",
default: ""
)}}Null Coalescing
{{coalesce(nickname, first_name)}} # First non-null
{{default(middle_name, "")}} # Default if null
{{optional(suffix)}} # Empty string if nullComputed Fields
For complex calculations:
{
"entities": {
"OrderItem": {
"fields": {
"quantity": { "generator": "quantity" },
"unit_price": { "generator": "price" },
"discount_rate": { "generator": "discount" },
"subtotal": { "computed": "unit_price * quantity" },
"discount_amount": { "computed": "subtotal * discount_rate" },
"total": { "computed": "subtotal - discount_amount" },
"tax": { "computed": "total * 0.18" },
"grand_total": { "computed": "total + tax" }
}
}
}
}Computed Syntax
{ "computed": "expression" }Available operators:
+ - * / %— Arithmetic== != < > <= >=— Comparison&& || !— Logical( )— Grouping
Examples:
{ "computed": "price * quantity" }
{ "computed": "price * (1 - discount_rate)" }
{ "computed": "if(quantity > 10, price * 0.9, price) * quantity" }
{ "computed": "round(total * tax_rate, 2)" }Uniqueness Constraints
unique
Ensure unique values:
{
"generators": {
"email": {
"type": "template",
"pattern": "{{lowercase(first_name)}}.{{lowercase(last_name)}}@{{pick(domains)}}",
"unique": true
}
}
}unique with fallback
Handle exhaustion:
{
"generators": {
"username": {
"type": "template",
"pattern": "{{lowercase(first_name)}}{{number:1-99}}",
"unique": true,
"unique_fallback": "user_{{uuid:v4}}"
}
}
}unique within scope
{
"generators": {
"product_code": {
"type": "template",
"pattern": "{{category.code}}-{{number:0001-9999}}",
"unique_within": "category"
}
}
}Full Example
{
"$schema": "https://phony.cloud/schemas/pdl-1.0.json",
"version": "1.0",
"locale": "tr_TR",
"generators": {
"first_name": {
"type": "model",
"source": "models/names.ngram"
},
"last_name": {
"type": "model",
"source": "models/surnames.ngram"
},
"country": {
"type": "list",
"source": "lists/countries.json"
},
"full_name": {
"type": "template",
"pattern": "{{first_name}} {{last_name}}"
},
"email": {
"type": "template",
"pattern": "{{lowercase(first_name)}}.{{lowercase(last_name)}}@{{pick(['gmail.com', 'yahoo.com', 'hotmail.com'])}}",
"unique": true
},
"phone": {
"type": "template",
"pattern": "{{country.phone_prefix}} {{pattern:### ### ## ##}}"
},
"username": {
"type": "template",
"variants": [
{ "pattern": "{{lowercase(first_name)}}{{number:1-999}}", "weight": 50 },
{ "pattern": "{{lowercase(first_name)}}.{{lowercase(last_name)}}", "weight": 30 },
{ "pattern": "{{lowercase(first_name)}}_{{number:1000-9999}}", "weight": 20 }
],
"unique": true
},
"address": {
"type": "template",
"variants": [
{ "pattern": "{{street_name}} No:{{number:1-200}} {{district}}/{{city}}", "weight": 40 },
{ "pattern": "{{neighborhood}} Mah. {{street_name}} No:{{number:1-150}}", "weight": 35 },
{ "pattern": "{{street_name}} {{number:1-100}}/{{number:1-20}} {{city}}", "weight": 25 }
]
}
},
"entities": {
"User": {
"fields": {
"id": {
"type": "logic",
"algorithm": "uuid_v7",
"primary_key": true
},
"first_name": { "generator": "first_name" },
"last_name": { "generator": "last_name" },
"full_name": { "generator": "full_name" },
"email": { "generator": "email" },
"phone": { "generator": "phone" },
"username": { "generator": "username" },
"address": { "generator": "address" },
"initials": {
"type": "template",
"pattern": "{{uppercase(substring(self.first_name, 0, 1))}}{{uppercase(substring(self.last_name, 0, 1))}}"
},
"created_at": {
"type": "logic",
"algorithm": "datetime_between",
"params": { "start": "-2years", "end": "now" }
},
"is_verified": {
"type": "logic",
"algorithm": "boolean",
"params": { "probability": 0.75 }
}
}
}
}
}Quick Reference
| Category | Syntax | Example |
|---|---|---|
| Reference | {{name}} | {{first_name}} |
| Nested | {{obj.prop}} | {{country.code}} |
| Number | {{number:min-max}} | {{number:1-100}} |
| Pattern | {{pattern:format}} | {{pattern:###-####}} |
| Pick | {{pick(list)}} | {{pick(colors)}} |
| Transform | {{func(val)}} | {{lowercase(name)}} |
| Conditional | {{if(cond,t,f)}} | {{if(age>18,"Adult","Minor")}} |
| Computed | computed: "expr" | computed: "price * qty" |