JexLang: Write Once, Validate Everywhere — A Platform-Agnostic Expression Language

Published: (December 13, 2025 at 01:51 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

🎯 The Problem We All Face

Picture this: You’re building a multi‑platform application—a React web app, an Android mobile app, a Spring Boot backend, and maybe an iOS app in the pipeline. Everything seems manageable until you hit a familiar wall — validation logic.

Your product manager walks in and says:

“We need to validate that users are 18+ years old, their email matches the company domain, and if they select Premium plan, they must have a valid credit card.”

Simple enough, right? Now multiply that across platforms:

// JavaScript (Web/React Native/NodeJS)
if (
  user.age >= 18 &&
  user.email.endsWith('@company.com') &&
  (user.plan !== 'Premium' || user.hasValidCard)
) {
  // valid
}
// Java (Backend/Android)
if (
  user.getAge() >= 18 &&
  user.getEmail().endsWith("@company.com") &&
  (!user.getPlan().equals("Premium") || user.hasValidCard())
) {
  // valid
}
// Swift (iOS)
if user.age >= 18 &&
   user.email.hasSuffix("@company.com") &&
   (user.plan != "Premium" || user.hasValidCard) {
  // valid
}

Now imagine you have 50+ validation rules across your application. Each platform has its own implementation, each needs to be tested, and each can harbor subtle bugs.

Welcome to validation hell. 🔥

😫 The Real Pain Points

1. Code Duplication Nightmare

Every validation rule exists in multiple places. When a business rule changes, you update code in 3‑4 different repositories. Miss one? Inconsistent behavior that haunts your debugging sessions.

2. Subtle Platform Differences

Each language handles things slightly differently:

  • String comparison: === vs .equals() vs ==
  • Null handling: undefined vs null vs Optional<>
  • Type coercion: JavaScript’s quirky "5" + 3 = "53" vs Java’s strict typing

These differences lead to bugs that appear only on specific platforms—the worst kind of bugs.

3. Hardcoded Logic = Deployment Pain

Changing a validation rule means a code change, PR review, testing, staging, and deployment—for each platform separately. What if the validation logic could live in your database or configuration? What if non‑developers could modify simple rules without touching code?

4. Testing Overhead

50 validation rules × 4 platforms = 200 test cases (at minimum). And you need to ensure they all behave identically. Good luck with that.

💡 What If There Was a Better Way?

What if you could write your validation logic once in a simple, familiar syntax:

user.age >= 18 && endsWith(user.email, "@company.com") && (user.plan != "Premium" || user.hasValidCard)

…and have it execute identically on:

  • ✅ JavaScript/TypeScript (Browser & Node.js)
  • ✅ Java (Android & Backend)
  • 🚧 Swift (iOS) — coming soon!

No platform‑specific quirks. No code duplication. No deployment overhead.

This is exactly why I built JexLang.

🚀 Introducing JexLang

JexLang is a lightweight, platform‑agnostic expression language with JavaScript‑like syntax. It solves one problem exceptionally well: write logic once, run it everywhere with consistent behavior.

Why JavaScript‑Like Syntax?

Most developers are familiar with JavaScript syntax. Even if you’re primarily a Java or Swift developer, you’ve likely encountered JavaScript at some point. By using a familiar syntax, JexLang has virtually zero learning curve.

If you can write this:

user.age >= 18 && user.active === true

you already know JexLang.

🛠️ How JexLang Works

JexLang consists of three core components:

1. Lexer (Tokenizer)

Breaks down your expression into tokens:

"user.age >= 18" → [IDENTIFIER:user, DOT, IDENTIFIER:age, GTE, NUMBER:18]

2. Parser

Converts tokens into an Abstract Syntax Tree (AST):

{
  "type": "BinaryExpression",
  "left": {
    "type": "MemberExpression",
    "object": "user",
    "property": "age"
  },
  "operator": ">=",
  "right": {
    "type": "NumericLiteral",
    "value": 18
  }
}

3. Interpreter

Evaluates the AST against your context data and returns the result.

The magic? The same AST structure and evaluation logic is implemented identically in each language—same parser rules, same operator precedence, same type coercion, same results.

📖 Real‑World Use Cases

1. Dynamic Form Validations

Store validation rules in your backend and share them across all platforms:

{
  "fields": [
    {
      "name": "email",
      "validations": [
        { "rule": "required(email)", "message": "Email is required" },
        { "rule": "contains(email, '@')", "message": "Please enter a valid email" }
      ]
    },
    {
      "name": "age",
      "validations": [
        { "rule": "age >= 18", "message": "You must be 18 or older" }
      ]
    }
  ]
}

Your React app, Android app, and backend all fetch these rules and evaluate them using JexLang. One source of truth.

2. Business Rule Engines

Configure business logic without code deployments:

// Rules stored in database
const discountRules = [
  { condition: "customer.tier === 'Gold' && cart.total > 100", discount: 20 },
  { condition: "customer.tier === 'Silver' && cart.total > 200", discount: 10 },
  { condition: "cart.items.length > 5", discount: 5 }
];

// Evaluate at runtime
const applicableRule = discountRules.find(rule =>
  jexlang.evaluate(rule.condition, { customer, cart })
);

Marketing wants to change the Gold tier threshold from 100 to 150? Update the database—no deployment needed.

3. Conditional UI Rendering

Show/hide UI elements based on configurable rules:

{
  "component": "PremiumBanner",
  "showWhen": "user.subscription === 'free' && user.daysActive > 30"
}

4. Workflow Automation

Build workflow engines where conditions are user‑configurable:

IF order.total > 1000 AND customer.verified === true
  → Route to priority fulfillment

IF order.items.length > 10
  → Apply bulk processing

IF contains(order.shippingAddress.country, 'EU')
  → Apply VAT calculation

5. Feature Flags with Complex Logic

Go beyond simple boolean flags:

const featureRules = {
  newCheckout: "user.region === 'US' && user.accountAge > 30 && !user.hasActiveIssues",
  betaFeatures: "user.role === 'developer' || contains(user.email, '@internal.com')"
};

⚡ Quick Examples

Basic Expressions

// Arithmetic
jexlang.evaluate("10 + 5 * 2"); // 20

// Comparisons
jexlang.evaluate("100 >= 50"); // true

// Logical operators
jexlang.evaluate("true && false || true"); // true

// String operations
jexlang.evaluate("'Hello' + ' ' + 'World'"); // "Hello World"

Working with Context

const context = {
  user: {
    name: "John Doe",
    age: 28,
    email: "john@company.com",
    roles: ["admin", "editor"]
  },
  settings: {
    maxUploadSize: 100,
    allowedFormats: ["jpg", "png", "pdf"]
  }
};

// Access nested properties
jexlang.evaluate("user.name", context); // "John Doe"
jexlang.evaluate("settings.maxUploadSize > 50", context); // true
Back to Blog

Related posts

Read more »

Chasing 240 FPS in LLM Chat UIs

TL;DR I built a benchmark suite to test various optimizations for streaming LLM responses in a React UI. Key takeaways: 1. Build a proper state first, then opt...