How to map FCC tower lease terms to JSON schemas

Legacy FCC tower lease agreements bury the exact facts an automated compliance pipeline needs — structural height caps, RF emission ceilings, municipal setbacks, remediation windows, and conditional variance rights — inside unstructured PDFs and scanned prose. While those terms stay in free text, every downstream automation fractures: zoning checks fall back to manual audit, renewal notices slip, and a mis-read height cap becomes a regulatory exposure nobody sees until an inspection. This page shows the concrete task of converting one raw lease dictionary into a strictly typed, machine-verifiable JSON artifact: define the schema, normalize vendor fields into a canonical namespace, validate every field, categorize each failure, and seal the record with an audit hash. It is the run-it-today implementation behind the controlled vocabularies defined in Lease Taxonomy Standardization, the parent topic that decides what every canonical field means; here we solve how a single lease is mapped, validated, and made auditable.

Prerequisites & Context

You need Python 3.10 or newer and one third-party package, jsonschema, installed with pip install jsonschema; everything else runs on the standard library. Before you map a single field, three things must already be settled:

  • A canonical field taxonomy. Field names, types, units, and enum values come from Lease Taxonomy Standardization, so that max_height_ft means the same measured quantity whether it arrived from a carrier feed, a county rider, or a hand-keyed legacy form. The JSON Schema you build here is the executable expression of that taxonomy.
  • A regulatory baseline. Lease terms cite specific rule parts — FCC Part 1, Part 22, and Part 27, and Title 47 CFR structural and RF provisions. Nested conditions are common: a lease may cap antenna mounting at 150 feet unless a municipal variance is granted, which then triggers a secondary environmental review. The schema separates base obligations from conditional modifiers so those overrides validate rather than corrupt.
  • A clear consumer contract. The validated artifact does not stop here. It routes to the Zoning Rule Engine Design, which expects deterministic boolean flags and numeric thresholds, not legal prose; sensitive fields are stripped under Security Boundary Configuration before anything reaches a public feed; and when source parsing degrades, Fallback Routing Protocols keep the pipeline moving rather than halting it.

With those in hand, this task sits inside the wider Telecom Tower Compliance Architecture & Data Mapping pipeline as the gate that turns a lease into a validated record everything downstream can trust.

From raw lease dictionary to a sealed, validated JSON artifact A raw lease dictionary flows into a namespace-normalization stage that aliases vendor field names and converts units, then into a Draft 7 validator. The validator forks: the failure branch (dashed) leads to categorized validation errors and raises a LeaseMappingError, while the success branch generates a SHA-256 audit hash and emits a validated JSON artifact. fail pass Raw lease dictionary Namespace normalization alias · convert units Draft 7 validator Categorized validation errors Raise LeaseMappingError SHA-256 audit hash Validated JSON artifact

Step-by-Step Implementation

Step 1 — Define the canonical schema with strict boundaries. Model base obligations as typed numeric fields with explicit units, and lock the object with additionalProperties: false so an unmapped vendor field is rejected rather than silently carried through.

python
LEASE_SCHEMA = {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "required": ["lease_id", "site_coordinates", "structural_limits", "compliance_state"],
    "additionalProperties": False,
    # ...properties defined in the full example below
}

Step 2 — Pin identifier formats with patterns. Lease and FCC identifiers have fixed shapes; encode them as regex so a malformed lease_id or a mis-typed ASR number fails at the boundary instead of contaminating a join downstream.

python
properties = {
    "lease_id": {"type": "string", "pattern": "^LSE-[A-Z0-9]{6}$"},
    "fcc_asr_number": {"type": "string", "pattern": "^[0-9]{7}$"},
}

Step 3 — Separate conditional modifiers from base obligations. A variance is a paired trigger and override value. Keep variance_granted (boolean) and variance_height_ft (numeric) as distinct fields so the Zoning Rule Engine Design can evaluate the override without re-parsing prose.

python
properties = {
    "structural_limits": {
        "type": "object",
        "required": ["max_height_ft"],
        "properties": {
            "max_height_ft": {"type": "number", "minimum": 0, "maximum": 500},
            "variance_granted": {"type": "boolean"},
            "variance_height_ft": {"type": "number", "minimum": 0, "maximum": 600},
        },
    },
}

Step 4 — Validate once, collect every error. Use Draft7Validator(...).iter_errors() rather than validate() so a lease with three problems reports all three in one pass instead of one-at-a-time.

python
validator = Draft7Validator(LEASE_SCHEMA)
errors = [categorize(e) for e in sorted(validator.iter_errors(raw_lease), key=lambda e: e.message)]

Step 5 — Categorize each failure operationally. Raw validator messages are for developers; operations needs categories it can route on. Map the validator keyword to MISSING_MANDATORY, TYPE_MISMATCH, BOUNDARY_VIOLATION, or SCHEMA_DRIFT.

Step 6 — Hash the canonical record for audit immutability. A validated lease gets a SHA-256 digest over its sorted-key serialization plus the schema version. When an FCC or municipal auditor later asks whether a stored lease matches the one ingested, the regenerated hash proves it — or exposes the tamper.

Complete Runnable Example

The diagram below traces the mapping path; the code under it is self-contained and runs on Python 3.10+ with only jsonschema installed. It carries the three mandatory pieces for this codebase — structured logging, a custom exception class, and a SHA-256 audit hash — and drives them with realistic identifiers (LSE-8X9A2B, FCC ASR 1287654, jurisdiction MUN-TX-014).

How the categorize() step routes each validator keyword to an operational category The Draft 7 validator's iter_errors pass forks on a decision node. The no-errors branch seals the record with a SHA-256 audit hash and emits a validated JSON artifact. The one-or-more-errors branch fans out to four category buckets keyed by the jsonschema keyword that triggered them — required maps to MISSING_MANDATORY, type and format map to TYPE_MISMATCH, minimum, maximum, pattern and enum map to BOUNDARY_VIOLATION, and additionalProperties maps to SCHEMA_DRIFT — and every failing record converges on a raised LeaseMappingError. none one or more Raw lease dictionary Draft 7 validator iter_errors() Validation errors? SHA-256 audit hash Validated JSON artifact MISSING_ MANDATORY required TYPE_MISMATCH type · format BOUNDARY_ VIOLATION min·max·pattern·enum SCHEMA_DRIFT additionalProperties Raise LeaseMappingError

Figure: schema validation routes each failure to a categorized error before any record is sealed.

python
import json
import hashlib
import logging
from typing import Dict, Any, List, Tuple
from jsonschema import ValidationError, Draft7Validator

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger("lease_mapper")

LEASE_SCHEMA = {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "required": ["lease_id", "site_coordinates", "structural_limits", "compliance_state"],
    "additionalProperties": False,
    "properties": {
        "lease_id": {"type": "string", "pattern": "^LSE-[A-Z0-9]{6}$"},
        "fcc_asr_number": {"type": "string", "pattern": "^[0-9]{7}$"},
        "jurisdiction_code": {"type": "string", "pattern": "^MUN-[A-Z]{2}-[0-9]{3}$"},
        "site_coordinates": {
            "type": "object",
            "required": ["lat", "lon"],
            "properties": {
                "lat": {"type": "number", "minimum": -90, "maximum": 90},
                "lon": {"type": "number", "minimum": -180, "maximum": 180},
            },
        },
        "structural_limits": {
            "type": "object",
            "required": ["max_height_ft"],
            "properties": {
                "max_height_ft": {"type": "number", "minimum": 0, "maximum": 500},
                "variance_granted": {"type": "boolean"},
                "variance_height_ft": {"type": "number", "minimum": 0, "maximum": 600},
            },
        },
        "compliance_state": {"type": "string",
            "enum": ["ACTIVE", "PENDING_VARIANCE", "UNDER_AUDIT", "EXPIRED"]},
        "remediation_deadline": {"type": "string", "format": "date"},
        "rf_emission_threshold_dbm": {"type": "number", "minimum": -100, "maximum": 0},
    },
}

class LeaseMappingError(Exception):
    """Raised when a raw lease payload fails schema validation."""

def categorize(error: ValidationError) -> Dict[str, Any]:
    path = ".".join(str(p) for p in error.absolute_path) or "root"
    if error.validator == "required":
        return {"category": "MISSING_MANDATORY", "field": path, "detail": error.message}
    if error.validator in ("type", "format"):
        return {"category": "TYPE_MISMATCH", "field": path, "detail": error.message}
    if error.validator in ("minimum", "maximum", "pattern", "enum"):
        return {"category": "BOUNDARY_VIOLATION", "field": path, "detail": error.message}
    if error.validator == "additionalProperties":
        return {"category": "SCHEMA_DRIFT", "field": path, "detail": "Unauthorized field(s)"}
    return {"category": "UNKNOWN_SCHEMA_ERROR", "field": path, "detail": error.message}

def audit_hash(payload: Dict[str, Any], schema_version: str = "v1.2") -> str:
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(f"{schema_version}|{canonical}".encode("utf-8")).hexdigest()

def map_lease_to_schema(raw_lease: Dict[str, Any]) -> Tuple[Dict[str, Any], str]:
    validator = Draft7Validator(LEASE_SCHEMA)
    errors: List[Dict[str, Any]] = [
        categorize(e) for e in sorted(validator.iter_errors(raw_lease), key=lambda e: e.message)
    ]
    if errors:
        log.warning("validation failed with %d error(s)", len(errors))
        raise LeaseMappingError(json.dumps(errors))
    digest = audit_hash(raw_lease)
    log.info("mapped %s audit=%s", raw_lease["lease_id"], digest[:16])
    return raw_lease, digest

if __name__ == "__main__":
    sample_lease = {
        "lease_id": "LSE-8X9A2B",
        "fcc_asr_number": "1287654",
        "jurisdiction_code": "MUN-TX-014",
        "site_coordinates": {"lat": 32.7767, "lon": -96.7970},
        "structural_limits": {"max_height_ft": 145.0, "variance_granted": False},
        "compliance_state": "ACTIVE",
        "rf_emission_threshold_dbm": -45.2,
    }
    try:
        payload, digest = map_lease_to_schema(sample_lease)
        print(json.dumps({"status": "VALID", "audit_id": digest[:16]}, indent=2))
    except LeaseMappingError as err:
        log.error("pipeline halted: %s", err)

Verification & Expected Output

Save the block as lease_map.py and run python3 lease_map.py. With the valid sample above you should see exactly:

text
INFO mapped LSE-8X9A2B audit=3f2a... (16-char digest)
{
  "status": "VALID",
  "audit_id": "..."
}

The audit_id is deterministic for these exact field values: re-run and it stays identical, because the hash is taken over the sorted-key serialization plus the schema version. If the digest changes between runs on unchanged input, something is mutating the payload before it is hashed. To see a failure, break the input on purpose — change "max_height_ft": 145.0 to 600.0. Validation now raises, and the categorized error reads {"category": "BOUNDARY_VIOLATION", "field": "structural_limits.max_height_ft", "detail": "600.0 is greater than the maximum of 500"}. Add an unmapped key such as "monthly_rent_usd": 4200 and you instead get a SCHEMA_DRIFT category, because additionalProperties: false refuses to pass a field the taxonomy never authorized. A lease missing compliance_state entirely surfaces as MISSING_MANDATORY with field: "root" — the required-field check reports against the object, not a leaf.

Gotchas & Edge Cases

  • FCC ASR number format drift. Antenna Structure Registration numbers are seven digits, but legacy leases record them with a leading A, embedded spaces, or as a URL fragment copied from the registration lookup. The ^[0-9]{7}$ pattern rejects all of those, which is correct — but it means normalization must strip non-digit characters before validation, not after, or a perfectly valid registration fails on formatting alone. Normalize, then validate.
  • Feet-versus-metres in the height cap. A municipal rider that expresses the structural limit in metres will validate happily against a max_height_ft bound if you map the raw number straight through: 150 metres reads as 150 feet and slips under the 500-foot ceiling while actually describing a 492-foot structure. Unit conversion has to happen during the namespace-normalization step, keyed off the source’s declared unit, before the value ever reaches the schema. A number that cannot be resolved to feet must raise, never default.
  • Unicode in scanned legacy PDFs. Contracts OCR’d from 2000s-era scans routinely carry non-breaking spaces, curly quotes, and en-dashes inside otherwise-numeric fields (145–150, a hyphenated range). These break float() coercion and pattern matches in ways that look like data errors but are really encoding artifacts. Sanitize to NFKC and strip control characters during ingestion so the validator judges the value, not the scanner’s noise.

FAQ

Why validate with iter_errors instead of jsonschema's validate()?

The one-shot validate() raises on the first violation it meets, so a lease with a bad height cap, a missing compliance state, and an unauthorized field forces three ingest-and-fix cycles to clear. Draft7Validator(...).iter_errors() returns the full set in a single pass, letting the pipeline categorize and report every problem at once. For a bulk portfolio import that difference is the gap between one remediation batch and dozens.

How does the schema handle a conditional variance that raises the height cap?

A variance is modelled as two separate fields rather than an overloaded one: variance_granted is a boolean trigger and variance_height_ft carries the override value with its own bound. The mapping layer's job is only to validate that both are well-typed and in range; the actual precedence logic — whether the override applies and whether a secondary environmental review is required — is evaluated later by the Zoning Rule Engine Design, which reads these as deterministic flags. Keeping the two concerns separated is what lets the schema stay declarative.

Why hash the record instead of just logging that it was validated?

A log proves a record passed validation on a given day; a SHA-256 hash over its canonical sorted-key serialization proves the stored record has not changed since. During an FCC or municipal audit the operator regenerates the digest from the archived lease and shows it matches the ingestion-day value, giving tamper-evident lineage that a plain log cannot. The same deterministic hash also de-duplicates re-submitted leases, since an identical lease produces an identical digest.

Related pages