schema-dsl optional tag? supported

📋 Function Overview

schema-dsl now supports explicit marking of optional fields using ?, providing clearer semantic expression.

Supported tags

markmeaningExampleDescription
!Requiredstring!Field must exist
?Optionalstring?Fields can be omitted (explicit expression)
UnmarkedOptional (default)stringFields can be omitted (default behavior)

✅Supported syntax

1. Basic type +?

import { s, validate } from 'schema-dsl/pure';

const schema = s({
  username: 'string!', // required string
  nickname: 'string', // optional string (default)
  bio: 'string?', // Explicit optional string
  email: 'email?' // Optional email
});

// verify
validate(schema, {}); // ✅ Passed (only username is required)
validate(schema, { username: 'test' }); // ✅ Passed
validate(schema, { username: 'test', bio: 'hi' }); // ✅ Passed
validate(schema, { username: 'test', email: 'invalid' }); // ❌ failed (email format error)

2. Constrained type +?

const schema = s({
  username: 'string:3-32!', // required, length 3-32
  nickname: 'string:3-32?', // optional, length 3-32 when valid
  age: 'number:18-?', // optional, if there is a value ≥ 18
  score: 'number:0-100?' // Optional, 0-100 when there is a value
});

validate(schema, { username: 'test' }); // ✅ Passed
validate(schema, { username: 'test', age: 16 }); // ❌ failed (age<18)
validate(schema, { username: 'test', age: 20 }); // ✅ Passed

3. Format type +?

const schema = s({
  email: 'email?', // optional email
  url: 'url?', // optional URL
  uuid: 'uuid?', // optional UUID
  date: 'date?', // optional date
  phone: 'phone:cn?' // Optional Chinese mobile phone number
});

validate(schema, {}); // ✅ Pass (all optional)
validate(schema, { email: 'test@example.com' }); // ✅ Passed
validate(schema, { email: 'invalid' }); // ❌ failed (format error)

4. Array type +?

const schema = s({
  tags: 'array<string>?', // Optional string array
  items: 'array:1-10?', // Optional array, length 1-10
  numbers: 'array<number>?' // optional array of numbers
});

validate(schema, {}); // ✅ Passed
validate(schema, { tags: ['a', 'b'] }); // ✅ Passed
validate(schema, { tags: [] }); // ✅ Pass (empty array)

🎯 Semantic contrast

string vs string?

Although both behave the same (both are optional), the semantics are different:

// Method 1: Implicitly optional (default)
const schema1 = s({
  nickname: 'string'
});

// Method 2: Explicitly optional (recommended)
const schema2 = s({
  nickname: 'string?'
});

Recommended scenarios for using ?:

  • Need to make it clear "This field is intentionally designed to be optional"
  • Improve code readability when comparing with other required fields
  • Team specs require optional fields to be explicitly marked

Example:

// ❌ Unclear: Which ones are intentionally optional? Which required marks are missing?
const schema = s({
  username: 'string!',
  nickname: 'string',
  bio: 'string',
  email: 'email!'
});

// ✅ Clarity: Clearly express design intent
const schema = s({
  username: 'string!', // required
  nickname: 'string?', // optional
  bio: 'string?', // optional
  email: 'email!' // required
});

⚠️ Notes

1.? in enumeration type?

Special attention is required when ? appears in an enumeration value:

// ❌ Error:? will be treated as part of the enumeration value
const schema1 = s({
  status: 'active|inactive?'
});
// Parsed as: enum ['active', 'inactive?']
// 'inactive' will fail validation!

// ✅ Correct: enums are optional by default
const schema2 = s({
  status: 'active|inactive'
});

// ✅ Correct: Use when enumeration is required!
const schema3 = s({
  status: 'active|inactive!'
});

2. Priority rules

When ! and ? are present at the same time (although not recommended), ! takes precedence:

// ⚠️ Not recommended: use both! and?
const schema = s({
  field: 'string!?' //! takes precedence, the field is required
});

3. Optional object fields

// The object itself is optional, internal fields are required
const schema1 = s({
  user: {
    name: 'string!', // When user exists, name is required
    email: 'email!' // When user exists, email is required
  }
});

// The object itself is optional (explicit), internal fields are required
const schema2 = s({
  'user?': { // Explicitly optional
    name: 'string!',
    email: 'email!'
  }
});

// The object itself is required, internal fields are optional
const schema3 = s({
  'user!': { // Object required
    name: 'string?', // optional
    email: 'email?' // optional
  }
});

📊 Actual test results

test statistics

  • string? - Supported
  • string:3-32? - Support
  • email? - Support
  • number:18-? - Support
  • array? - Supported
  • Relevant unit tests have been covered

test code

import { s, validate } from 'schema-dsl/pure';

//Test 1: string?
const schema1 = s({ name: 'string?' });
console.log(validate(schema1, {}).valid);              // true
console.log(validate(schema1, { name: 'test' }).valid); // true

//Test 2: email?
const schema2 = s({ email: 'email?' });
console.log(validate(schema2, {}).valid);                        // true
console.log(validate(schema2, { email: 'test@ex.com' }).valid); // true
console.log(validate(schema2, { email: 'invalid' }).valid);     // false ✅

//Test 3: string:3-32?
const schema3 = s({ username: 'string:3-32?' });
console.log(validate(schema3, {}).valid);                   // true
console.log(validate(schema3, { username: 'ab' }).valid);   // false ✅
console.log(validate(schema3, { username: 'test' }).valid); // true

🔧 Implementation details

DslParser / DslBuilder markup processing

// DslParser.parseString()
if (s.endsWith('!')) {
  required = true;
  s = s.slice(0, -1);
} else if (s.endsWith('?')) {
  s = s.slice(0, -1);
}

// DslBuilder constructor (compatible with chain entry)
this._required = s.endsWith('!');
this._optional = s.endsWith('?') && !this._required;
if (this._required || this._optional) s = s.slice(0, -1);
}

The current version will uniformly strip the trailing ! / ? in DslParser.parseString(), while the DslBuilder constructor retains the same compatibility processing, so both the string DSL and the chain builder can recognize the optional tag.


📝 Best Practices

import { s } from 'schema-dsl/pure';

// ✅ Recommended: Mark all fields explicitly
const schema = s({
  // Required fields - use!
  username: 'string:3-32!',
  password: 'string:8-!',
  email: 'email!',

  // Optional fields - use?
  nickname: 'string:3-32?',
  bio: 'string:500?',
  avatar: 'url?',
  phone: 'phone:cn?',

  //Object fields
  'profile!': { // Object required
    age: 'number:18-?', // Age is optional
    gender: 'male|female|other?', // Gender is optional
  }
});

Team convention

If your team chooses explicit optional markers, use ? consistently for fields that are intentionally optional. Be careful with enum DSL such as active|inactive?: the ? belongs to the last enum value there, not to the whole field.


🔄 Version compatibility

  • v1.1.3 and before: ? is ignored, but does not affect functionality (because it is optional by default)
  • v1.1.4+: ? is processed explicitly, the semantics are clearer

Backwards Compatible: ✅ Fully compatible, no modifications required to all existing code


Corresponding sample file

Example entry: optional-marker-guide.ts Description: Cover the basic fields, object fields and default optional enumeration scenarios of ! / ? to directly display the success/failure path.