Skip to content

Serialization

The serialization API converts runtime annotated types to and from a plain JSON format. This enables transferring type definitions between backend and frontend, storing them in databases, or caching compiled types.

typescript
import {
  serializeAnnotatedType,
  deserializeAnnotatedType,
  buildJsonSchema,
} from '@atscript/typescript/utils'

Purpose

Serialize type definitions on the server and send them to the client. The client deserializes them and uses them for validation, live form tools, or schema-driven UI helpers without bundling the original .as files.

Basic Usage

typescript
import { Product } from './product.as'

// Serialize to a JSON-safe object
const serialized = serializeAnnotatedType(Product)
const json = JSON.stringify(serialized)

// ... transmit, store, or cache ...

// Deserialize back to a live type
const restored = deserializeAnnotatedType(JSON.parse(json))

// The restored type is fully functional
restored.validator().validate(data)
buildJsonSchema(restored)

Deserialized Types Are Live

A deserialized type is a fully functional TAtscriptAnnotatedType:

  • .validator() creates a working Validator instance
  • Works with buildJsonSchema() and forAnnotatedType()
  • isAnnotatedType() returns true
  • Metadata is accessible via .metadata.get()
  • The id field (type name) is preserved through serialization, so buildJsonSchema will still produce $defs/$ref for deserialized types

Versioning

The serialized output includes a $v field with the format version (currently 1). If the format changes in a future release, deserializeAnnotatedType() will throw when it encounters an incompatible version, so you know to re-serialize from the source types.

typescript
import { SERIALIZE_VERSION } from '@atscript/typescript/utils'
// SERIALIZE_VERSION === 1

Filtering Annotations

Use TSerializeOptions to control which annotations are included in the output. This is useful for stripping sensitive or server-only metadata before sending types to the client.

Strip specific annotations:

typescript
const serialized = serializeAnnotatedType(Product, {
  ignoreAnnotations: ['db.table', 'db.mongo.collection'],
})

Transform annotations with a callback:

typescript
const serialized = serializeAnnotatedType(Product, {
  processAnnotation({ key, value, path, kind }) {
    // Only keep meta.*, expect.*, and ui.* annotations
    if (key.startsWith('meta.') || key.startsWith('expect.') || key.startsWith('ui.')) {
      return { key, value }
    }
    // Return undefined to strip
  },
})

The processAnnotation callback receives:

  • key — annotation name (e.g. 'meta.label')
  • value — annotation value
  • path — property path as a string[] array (e.g. ['address', 'city'])
  • kind — type kind at this node ('', 'object', 'array', etc.)

Example: Server-Driven Field Tools

A practical use case: the server serializes a type definition and the client uses it to build a field list with labels and placeholders.

Server (Express endpoint):

typescript
import { User } from './user.as'
import { serializeAnnotatedType } from '@atscript/typescript/utils'

app.get('/api/form/user', (req, res) => {
  const schema = serializeAnnotatedType(User, {
    ignoreAnnotations: ['db.table', 'db.mongo.collection'], // strip server-only metadata
  })
  res.json(schema)
})

Client (Vue component):

vue
<script setup>
import { ref, onMounted } from 'vue'
import { deserializeAnnotatedType } from '@atscript/typescript/utils'

const fields = ref([])
const formData = ref({})

onMounted(async () => {
  const res = await fetch('/api/form/user')
  const type = deserializeAnnotatedType(await res.json())

  // Build UI field data from type metadata
  for (const [name, prop] of type.type.props.entries()) {
    fields.value.push({
      name,
      label: prop.metadata.get('meta.label') || name,
      placeholder: prop.metadata.get('ui.placeholder') || '',
    })
    formData.value[name] = ''
  }
})
</script>

<template>
  <form>
    <div v-for="field in fields" :key="field.name">
      <label>{{ field.label }}</label>
      <input v-model="formData[field.name]" :placeholder="field.placeholder" />
    </div>
  </form>
</template>

The field list, labels, and placeholders are all driven by annotations defined in the .as file, so the client does not need to duplicate that configuration.

Next Steps

Released under the MIT License.