Skip to content

Complex Objects

This guide demonstrates how to use S7E with complex object hierarchies, inheritance, and polymorphic serialization.

Inheritance and Polymorphism

S7E supports polymorphic serialization with automatic type discrimination.

typescript
import { S7e, JsonClass, JsonProperty } from 's7e';

// Base class
@JsonClass({ name: 'Shape' })
abstract class Shape {
  @JsonProperty({ name: 'id', type: String })
  public id: string;

  @JsonProperty({ name: 'color', type: String })
  public color: string;

  @JsonProperty({ name: 'position', type: Object })
  public position: { x: number; y: number };

  constructor(id: string, color: string, x: number = 0, y: number = 0) {
    this.id = id;
    this.color = color;
    this.position = { x, y };
  }

  abstract area(): number;
  abstract perimeter(): number;
}

// Circle implementation
@JsonClass({ name: 'Circle' })
class Circle extends Shape {
  @JsonProperty({ name: 'radius', type: Number })
  public radius: number;

  constructor(id: string, color: string, radius: number, x?: number, y?: number) {
    super(id, color, x, y);
    this.radius = radius;
  }

  area(): number {
    return Math.PI * this.radius * this.radius;
  }

  perimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}

// Rectangle implementation
@JsonClass({ name: 'Rectangle' })
class Rectangle extends Shape {
  @JsonProperty({ name: 'width', type: Number })
  public width: number;

  @JsonProperty({ name: 'height', type: Number })
  public height: number;

  constructor(id: string, color: string, width: number, height: number, x?: number, y?: number) {
    super(id, color, x, y);
    this.width = width;
    this.height = height;
  }

  area(): number {
    return this.width * this.height;
  }

  perimeter(): number {
    return 2 * (this.width + this.height);
  }
}

// Triangle implementation
@JsonClass({ name: 'Triangle' })
class Triangle extends Shape {
  @JsonProperty({ name: 'base', type: Number })
  public base: number;

  @JsonProperty({ name: 'height', type: Number })
  public height: number;

  @JsonProperty({ name: 'sideA', type: Number })
  public sideA: number;

  @JsonProperty({ name: 'sideB', type: Number })
  public sideB: number;

  constructor(id: string, color: string, base: number, height: number, sideA: number, sideB: number, x?: number, y?: number) {
    super(id, color, x, y);
    this.base = base;
    this.height = height;
    this.sideA = sideA;
    this.sideB = sideB;
  }

  area(): number {
    return 0.5 * this.base * this.height;
  }

  perimeter(): number {
    return this.base + this.sideA + this.sideB;
  }
}

// Register types for polymorphic deserialization
S7e.registerTypes([Circle, Rectangle, Triangle]);

// Usage example
const shapes: Shape[] = [
  new Circle('circle1', 'red', 5, 10, 20),
  new Rectangle('rect1', 'blue', 10, 15, 30, 40),
  new Triangle('tri1', 'green', 8, 6, 7, 9, 50, 60)
];

console.log('Original shapes:');
shapes.forEach(shape => {
  console.log(`${shape.constructor.name}: Area = ${shape.area()}, Perimeter = ${shape.perimeter()}`);
});

// Serialize array of polymorphic objects
const shapesJson = S7e.serializeArray(shapes);
console.log('\\nSerialized JSON:', shapesJson);

// Deserialize back to correct types
const deserializedShapes = S7e.deserializeArray(Shape, shapesJson);
console.log('\\nDeserialized shapes:');
deserializedShapes.forEach(shape => {
  console.log(`${shape.constructor.name}: Area = ${shape.area()}, Perimeter = ${shape.perimeter()}`);
  console.log(`Type check - Circle: ${shape instanceof Circle}, Rectangle: ${shape instanceof Rectangle}, Triangle: ${shape instanceof Triangle}`);
});

Nested Object Hierarchies

Complex nested structures with multiple levels of composition.

typescript
@JsonClass({ name: 'Address' })
class Address {
  @JsonProperty({ name: 'street', type: String })
  public street: string;

  @JsonProperty({ name: 'city', type: String })
  public city: string;

  @JsonProperty({ name: 'state', type: String })
  public state: string;

  @JsonProperty({ name: 'zipCode', type: String })
  public zipCode: string;

  @JsonProperty({ name: 'country', type: String })
  public country: string;

  @JsonProperty({ name: 'coordinates', type: Object, optional: true })
  public coordinates?: { latitude: number; longitude: number };

  constructor(street: string, city: string, state: string, zipCode: string, country: string = 'USA') {
    this.street = street;
    this.city = city;
    this.state = state;
    this.zipCode = zipCode;
    this.country = country;
  }

  public getFullAddress(): string {
    return `${this.street}, ${this.city}, ${this.state} ${this.zipCode}, ${this.country}`;
  }
}

@JsonClass({ name: 'ContactInfo' })
class ContactInfo {
  @JsonProperty({ name: 'email', type: String })
  public email: string;

  @JsonProperty({ name: 'phone', type: String, optional: true })
  public phone?: string;

  @JsonProperty({ name: 'mobile', type: String, optional: true })
  public mobile?: string;

  @JsonProperty({ name: 'fax', type: String, optional: true })
  public fax?: string;

  @JsonProperty({ name: 'website', type: String, optional: true })
  public website?: string;

  constructor(email: string) {
    this.email = email;
  }
}

@JsonClass({ name: 'Department' })
class Department {
  @JsonProperty({ name: 'id', type: String })
  public id: string;

  @JsonProperty({ name: 'name', type: String })
  public name: string;

  @JsonProperty({ name: 'description', type: String, optional: true })
  public description?: string;

  @JsonProperty({ name: 'budget', type: Number, optional: true })
  public budget?: number;

  constructor(id: string, name: string) {
    this.id = id;
    this.name = name;
  }
}

@JsonClass({ name: 'Employee' })
class Employee {
  @JsonProperty({ name: 'id', type: Number })
  public id: number;

  @JsonProperty({ name: 'firstName', type: String })
  public firstName: string;

  @JsonProperty({ name: 'lastName', type: String })
  public lastName: string;

  @JsonProperty({ name: 'title', type: String })
  public title: string;

  @JsonProperty({ name: 'department', type: Department })
  public department: Department;

  @JsonProperty({ name: 'manager', type: Employee, optional: true })
  public manager?: Employee;

  @JsonProperty({ name: 'directReports', type: [Employee] })
  public directReports: Employee[];

  @JsonProperty({ name: 'contact', type: ContactInfo })
  public contact: ContactInfo;

  @JsonProperty({ name: 'address', type: Address })
  public address: Address;

  @JsonProperty({ name: 'salary', type: Number, optional: true })
  public salary?: number;

  @JsonProperty({ name: 'startDate', type: Date })
  public startDate: Date;

  @JsonProperty({ name: 'endDate', type: Date, optional: true })
  public endDate?: Date;

  @JsonProperty({ name: 'skills', type: [String] })
  public skills: string[];

  @JsonProperty({ name: 'projects', type: [String] })
  public projects: string[];

  constructor(
    id: number,
    firstName: string,
    lastName: string,
    title: string,
    department: Department,
    contact: ContactInfo,
    address: Address
  ) {
    this.id = id;
    this.firstName = firstName;
    this.lastName = lastName;
    this.title = title;
    this.department = department;
    this.contact = contact;
    this.address = address;
    this.directReports = [];
    this.startDate = new Date();
    this.skills = [];
    this.projects = [];
  }

  public getFullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }

  public addDirectReport(employee: Employee): void {
    if (!this.directReports.find(e => e.id === employee.id)) {
      this.directReports.push(employee);
      employee.manager = this;
    }
  }

  public removeDirectReport(employeeId: number): void {
    const index = this.directReports.findIndex(e => e.id === employeeId);
    if (index >= 0) {
      this.directReports[index].manager = undefined;
      this.directReports.splice(index, 1);
    }
  }

  public addSkill(skill: string): void {
    if (!this.skills.includes(skill)) {
      this.skills.push(skill);
    }
  }

  public addProject(project: string): void {
    if (!this.projects.includes(project)) {
      this.projects.push(project);
    }
  }
}

// Create complex organizational structure
const engineeringDept = new Department('eng', 'Engineering');
engineeringDept.description = 'Software development and engineering';
engineeringDept.budget = 2000000;

const marketingDept = new Department('mkt', 'Marketing');
marketingDept.description = 'Product marketing and communications';
marketingDept.budget = 500000;

// Create addresses
const johnAddress = new Address('123 Main St', 'San Francisco', 'CA', '94105');
johnAddress.coordinates = { latitude: 37.7749, longitude: -122.4194 };

const janeAddress = new Address('456 Oak Ave', 'Palo Alto', 'CA', '94301');
janeAddress.coordinates = { latitude: 37.4419, longitude: -122.1430 };

const bobAddress = new Address('789 Pine St', 'Mountain View', 'CA', '94041');

// Create contact info
const johnContact = new ContactInfo('john.doe@company.com');
johnContact.phone = '555-0123';
johnContact.mobile = '555-0124';

const janeContact = new ContactInfo('jane.smith@company.com');
janeContact.mobile = '555-0125';

const bobContact = new ContactInfo('bob.johnson@company.com');
bobContact.phone = '555-0126';

// Create employees
const cto = new Employee(1, 'John', 'Doe', 'CTO', engineeringDept, johnContact, johnAddress);
cto.salary = 200000;
cto.addSkill('Leadership');
cto.addSkill('Architecture');
cto.addSkill('Strategy');
cto.addProject('Platform Modernization');
cto.addProject('Team Scaling');

const seniorDev = new Employee(2, 'Jane', 'Smith', 'Senior Developer', engineeringDept, janeContact, janeAddress);
seniorDev.salary = 150000;
seniorDev.addSkill('TypeScript');
seniorDev.addSkill('React');
seniorDev.addSkill('Node.js');
seniorDev.addProject('Frontend Redesign');
seniorDev.addProject('API Development');

const juniorDev = new Employee(3, 'Bob', 'Johnson', 'Junior Developer', engineeringDept, bobContact, bobAddress);
juniorDev.salary = 90000;
juniorDev.addSkill('JavaScript');
juniorDev.addSkill('HTML/CSS');
juniorDev.addProject('Bug Fixes');
juniorDev.addProject('Documentation');

// Build hierarchy
cto.addDirectReport(seniorDev);
seniorDev.addDirectReport(juniorDev);

console.log('Organization Structure:');
console.log(`${cto.getFullName()} (${cto.title})`);
console.log(`  Department: ${cto.department.name}`);
console.log(`  Address: ${cto.address.getFullAddress()}`);
console.log(`  Skills: ${cto.skills.join(', ')}`);
console.log(`  Direct Reports: ${cto.directReports.length}`);

cto.directReports.forEach(report => {
  console.log(`    ${report.getFullName()} (${report.title})`);
  console.log(`      Skills: ${report.skills.join(', ')}`);
  console.log(`      Projects: ${report.projects.join(', ')}`);

  if (report.directReports.length > 0) {
    report.directReports.forEach(subReport => {
      console.log(`      └─ ${subReport.getFullName()} (${subReport.title})`);
    });
  }
});

// Serialize the entire organization
const orgJson = S7e.serialize(cto);
console.log('\\nSerialized organization size:', orgJson.length, 'characters');

// Deserialize back to objects
const deserializedCto = S7e.deserialize(Employee, orgJson);
console.log('\\nDeserialized CTO:', deserializedCto.getFullName());
console.log('Manager check:', deserializedCto.directReports[0].manager?.getFullName());
console.log('Nested manager check:', deserializedCto.directReports[0].directReports[0].manager?.getFullName());

// Verify complex relationships are preserved
const deserializedSeniorDev = deserializedCto.directReports[0];
const deserializedJuniorDev = deserializedSeniorDev.directReports[0];

console.log('\\nRelationship verification:');
console.log('CTO has senior dev as direct report:', deserializedCto.directReports.includes(deserializedSeniorDev));
console.log('Senior dev has CTO as manager:', deserializedSeniorDev.manager === deserializedCto);
console.log('Junior dev has senior dev as manager:', deserializedJuniorDev.manager === deserializedSeniorDev);

// Verify all nested objects are correct instances
console.log('\\nInstance checks:');
console.log('CTO is Employee:', deserializedCto instanceof Employee);
console.log('Department is Department:', deserializedCto.department instanceof Department);
console.log('Contact is ContactInfo:', deserializedCto.contact instanceof ContactInfo);
console.log('Address is Address:', deserializedCto.address instanceof Address);

Composition Patterns

Examples showing different composition patterns and how S7E handles them.

typescript
// Component-based architecture
@JsonClass({ name: 'Transform' })
class Transform {
  @JsonProperty({ name: 'position', type: Object })
  public position: { x: number; y: number; z: number };

  @JsonProperty({ name: 'rotation', type: Object })
  public rotation: { x: number; y: number; z: number };

  @JsonProperty({ name: 'scale', type: Object })
  public scale: { x: number; y: number; z: number };

  constructor() {
    this.position = { x: 0, y: 0, z: 0 };
    this.rotation = { x: 0, y: 0, z: 0 };
    this.scale = { x: 1, y: 1, z: 1 };
  }
}

@JsonClass({ name: 'Material' })
class Material {
  @JsonProperty({ name: 'color', type: String })
  public color: string;

  @JsonProperty({ name: 'texture', type: String, optional: true })
  public texture?: string;

  @JsonProperty({ name: 'opacity', type: Number })
  public opacity: number;

  @JsonProperty({ name: 'metallic', type: Number })
  public metallic: number;

  @JsonProperty({ name: 'roughness', type: Number })
  public roughness: number;

  constructor(color: string = '#ffffff') {
    this.color = color;
    this.opacity = 1.0;
    this.metallic = 0.0;
    this.roughness = 0.5;
  }
}

@JsonClass({ name: 'Mesh' })
class Mesh {
  @JsonProperty({ name: 'vertices', type: [Number] })
  public vertices: number[];

  @JsonProperty({ name: 'indices', type: [Number] })
  public indices: number[];

  @JsonProperty({ name: 'normals', type: [Number] })
  public normals: number[];

  @JsonProperty({ name: 'uvs', type: [Number], optional: true })
  public uvs?: number[];

  constructor() {
    this.vertices = [];
    this.indices = [];
    this.normals = [];
  }
}

@JsonClass({ name: 'GameObject' })
class GameObject {
  @JsonProperty({ name: 'id', type: String })
  public id: string;

  @JsonProperty({ name: 'name', type: String })
  public name: string;

  @JsonProperty({ name: 'active', type: Boolean })
  public active: boolean;

  @JsonProperty({ name: 'transform', type: Transform })
  public transform: Transform;

  @JsonProperty({ name: 'material', type: Material, optional: true })
  public material?: Material;

  @JsonProperty({ name: 'mesh', type: Mesh, optional: true })
  public mesh?: Mesh;

  @JsonProperty({ name: 'children', type: [GameObject] })
  public children: GameObject[];

  @JsonProperty({ name: 'parent', type: GameObject, optional: true })
  public parent?: GameObject;

  @JsonProperty({ name: 'tags', type: [String] })
  public tags: string[];

  @JsonProperty({ name: 'metadata', type: Object, optional: true })
  public metadata?: Record<string, any>;

  constructor(id: string, name: string) {
    this.id = id;
    this.name = name;
    this.active = true;
    this.transform = new Transform();
    this.children = [];
    this.tags = [];
  }

  public addChild(child: GameObject): void {
    if (!this.children.includes(child)) {
      this.children.push(child);
      child.parent = this;
    }
  }

  public removeChild(child: GameObject): void {
    const index = this.children.indexOf(child);
    if (index >= 0) {
      this.children.splice(index, 1);
      child.parent = undefined;
    }
  }

  public addTag(tag: string): void {
    if (!this.tags.includes(tag)) {
      this.tags.push(tag);
    }
  }

  public hasTag(tag: string): boolean {
    return this.tags.includes(tag);
  }

  public findChildByName(name: string): GameObject | undefined {
    return this.children.find(child => child.name === name);
  }

  public findChildrenWithTag(tag: string): GameObject[] {
    return this.children.filter(child => child.hasTag(tag));
  }

  public getAllDescendants(): GameObject[] {
    const descendants: GameObject[] = [];

    function collectDescendants(obj: GameObject) {
      for (const child of obj.children) {
        descendants.push(child);
        collectDescendants(child);
      }
    }

    collectDescendants(this);
    return descendants;
  }
}

// Create a complex scene hierarchy
const scene = new GameObject('scene', 'Main Scene');

// Create a car with wheels
const car = new GameObject('car', 'Sports Car');
car.addTag('vehicle');
car.addTag('interactive');
car.transform.position = { x: 0, y: 0, z: 0 };
car.material = new Material('#ff0000');
car.material.metallic = 0.8;
car.material.roughness = 0.2;

// Create wheels
const wheelPositions = [
  { name: 'Front Left Wheel', pos: { x: -1, y: -0.5, z: 1 } },
  { name: 'Front Right Wheel', pos: { x: 1, y: -0.5, z: 1 } },
  { name: 'Rear Left Wheel', pos: { x: -1, y: -0.5, z: -1 } },
  { name: 'Rear Right Wheel', pos: { x: 1, y: -0.5, z: -1 } }
];

wheelPositions.forEach(wheelData => {
  const wheel = new GameObject(`wheel_${wheelData.name.replace(/\\s/g, '_').toLowerCase()}`, wheelData.name);
  wheel.addTag('wheel');
  wheel.addTag('rotating');
  wheel.transform.position = wheelData.pos;
  wheel.material = new Material('#333333');
  wheel.material.metallic = 0.1;
  wheel.material.roughness = 0.8;
  car.addChild(wheel);
});

// Create environment objects
const ground = new GameObject('ground', 'Ground Plane');
ground.addTag('environment');
ground.addTag('static');
ground.transform.scale = { x: 100, y: 1, z: 100 };
ground.material = new Material('#654321');
ground.material.roughness = 0.9;

const building = new GameObject('building', 'Office Building');
building.addTag('environment');
building.addTag('static');
building.transform.position = { x: 10, y: 0, z: 10 };
building.transform.scale = { x: 5, y: 20, z: 5 };
building.material = new Material('#cccccc');

// Create building details
const windows = new GameObject('windows', 'Building Windows');
windows.addTag('detail');
windows.material = new Material('#87ceeb');
windows.material.opacity = 0.7;
building.addChild(windows);

const door = new GameObject('door', 'Main Entrance');
door.addTag('interactive');
door.addTag('door');
door.transform.position = { x: 0, y: 0, z: 2.5 };
door.material = new Material('#8b4513');
building.addChild(door);

// Add everything to scene
scene.addChild(ground);
scene.addChild(car);
scene.addChild(building);

// Add metadata
scene.metadata = {
  version: '1.0',
  created: new Date().toISOString(),
  author: 'Scene Builder',
  description: 'A simple car and building scene'
};

console.log('Scene Structure:');
console.log(`Scene: ${scene.name}`);
console.log(`Total objects in scene: ${scene.getAllDescendants().length + 1}`);

function printHierarchy(obj: GameObject, indent: string = '') {
  console.log(`${indent}${obj.name} [${obj.tags.join(', ')}]`);
  obj.children.forEach(child => {
    printHierarchy(child, indent + '  ');
  });
}

printHierarchy(scene);

// Find objects by tag
const vehicles = scene.getAllDescendants().filter(obj => obj.hasTag('vehicle'));
const wheels = scene.getAllDescendants().filter(obj => obj.hasTag('wheel'));
const interactive = scene.getAllDescendants().filter(obj => obj.hasTag('interactive'));

console.log(`\\nVehicles: ${vehicles.length}`);
console.log(`Wheels: ${wheels.length}`);
console.log(`Interactive objects: ${interactive.length}`);

// Serialize the entire scene
const sceneJson = S7e.serialize(scene);
console.log(`\\nSerialized scene size: ${sceneJson.length} characters`);

// Deserialize and verify
const deserializedScene = S7e.deserialize(GameObject, sceneJson);
console.log(`\\nDeserialized scene: ${deserializedScene.name}`);
console.log(`Total objects after deserialization: ${deserializedScene.getAllDescendants().length + 1}`);

// Verify relationships
const deserializedCar = deserializedScene.findChildByName('Sports Car');
const deserializedWheels = deserializedCar?.findChildrenWithTag('wheel') || [];
console.log(`Car found: ${deserializedCar ? 'Yes' : 'No'}`);
console.log(`Wheels attached to car: ${deserializedWheels.length}`);

// Verify parent-child relationships
const firstWheel = deserializedWheels[0];
if (firstWheel) {
  console.log(`First wheel parent is car: ${firstWheel.parent === deserializedCar}`);
}

// Verify all instances are correct
console.log(`\\nInstance verification:`);
console.log(`Scene is GameObject: ${deserializedScene instanceof GameObject}`);
console.log(`Scene transform is Transform: ${deserializedScene.transform instanceof Transform}`);
if (deserializedCar?.material) {
  console.log(`Car material is Material: ${deserializedCar.material instanceof Material}`);
}

Next Steps

These examples demonstrate S7E's powerful capabilities with complex object structures. Continue exploring:

Released under the MIT License.