Skip to content

JSON and XML Data Binding in Winter Boot with PHP 8

Winter Boot maps JSON and XML payloads onto typed PHP objects using attributes. Annotate model properties with #[JsonProperty] or the PAXB attributes (#[XmlElement], #[XmlAttribute], and others), and ObjectCreator — or your REST controller — fills them in for you, including nested objects and typed lists. One DTO class can carry both JSON and XML attributes and accept either format, depending on the request’s Content-Type.

Apply #[JsonProperty] to any property you want to bind from a JSON payload. The framework maps the external JSON key to the PHP property and performs type coercion automatically.

src/Model/Product.php
use dev\winterframework\stereotype\JsonProperty;
class Product {
#[JsonProperty] // uses the property name "name" by default
private string $name;
#[JsonProperty(name: 'unit_price')] // explicit external key
private float $price;
#[JsonProperty(required: true)]
private string $sku;
#[JsonProperty(name: 'tags', listClass: Tag::class)]
private array $tags = []; // list of nested objects
public function getName(): string { return $this->name; }
}
  • name string | array (default: property name) The external JSON key name(s) to bind. Accepts a single string or an array of aliases — the first matching alias in the payload wins.

  • required bool (default: false) When true, an InvalidSyntaxException is thrown if the key is absent from the payload. Automatically set to true when the property has no default value and its type does not allow null.

  • nillable bool (default: false) Allows null values to be set on the property. Automatically set to true for nullable types (?string, etc.).

  • listClass string (default: "") Fully qualified class name for the items when the property is a typed array. Each array element in the JSON payload is deserialized into the given class.

  • validate array (default: []) An array of validator names or [validatorName, ...options] tuples applied to the property value after binding.

Explicit Deserialization with ObjectCreator

Section titled “Explicit Deserialization with ObjectCreator”

Use ObjectCreator::createObject() to deserialize an associative array (e.g. the result of json_decode(..., true)) into a typed object:

use dev\winterframework\reflection\ObjectCreator;
$data = json_decode('{"name": "Pen", "unit_price": 2.01, "sku": "PEN-001"}', true);
$product = ObjectCreator::createObject(Product::class, $data);
echo $product->getName(); // "Pen"

When a controller method parameter is annotated with #[RequestBody] and the request carries Content-Type: application/json, the framework calls ObjectCreator::createObject() for you:

src/Controller/ProductController.php
use dev\winterframework\stereotype\RestController;
use dev\winterframework\stereotype\web\RequestMapping;
use dev\winterframework\enums\RequestMethod;
use dev\winterframework\web\MediaType;
use dev\winterframework\stereotype\web\RequestBody;
use dev\winterframework\web\http\ResponseEntity;
#[RestController]
class ProductController {
#[RequestMapping(
path: '/api/v1/products',
method: [RequestMethod::POST],
consumes: [MediaType::APPLICATION_JSON]
)]
public function createProduct(
#[RequestBody] Product $product
): ResponseEntity {
// $product is fully populated from the JSON request body
return ResponseEntity::ok()->withJson(['id' => 42]);
}
}

Winter Boot’s PAXB (PHP Architecture for XML Binding) mirrors Java’s JAXB: a set of attributes that describe how a class maps to an XML document. Annotate your class with #[XmlRootElement] and its properties with the appropriate XML attributes.

src/Model/Product.php
use dev\winterframework\paxb\attr\XmlRootElement;
use dev\winterframework\paxb\attr\XmlElement;
use dev\winterframework\paxb\attr\XmlAttribute;
#[XmlRootElement(name: 'product')]
class Product {
#[XmlAttribute(name: 'sku')]
private string $sku;
#[XmlElement(name: 'name')]
private string $name;
#[XmlElement(name: 'unit_price')]
private float $price;
}

The class above maps to the following XML document:

<product sku="PEN-001">
<name>Pen</name>
<unit_price>2.01</unit_price>
</product>
  • name string (default: class short name) The XML root element tag name. Place this attribute on the class itself.
#[XmlAnyElement]

Applied to a property, #[XmlAnyElement] captures any child elements not matched by other bindings. When lax is true, unknown elements are silently skipped rather than raising an error.

#[XmlAnyAttribute]

Applied to a property, #[XmlAnyAttribute] captures any XML attributes not matched by other bindings into an array on the property.

#[XmlPropertyOrder]

Applied to a class, #[XmlPropertyOrder] controls the order of child elements when writing XML. The order parameter lists property names in the desired output order. Set ignoreUnknown to true to silently drop unrecognized elements when reading.

Use ObjectCreator::createObjectXml() to parse an XML string directly into a typed object:

use dev\winterframework\reflection\ObjectCreator;
$xml = <<<XML
<product sku="PEN-001">
<name>Pen</name>
<unit_price>2.01</unit_price>
</product>
XML;
$product = ObjectCreator::createObjectXml(Product::class, $xml);

When you need finer control over parser properties, use XmlObjectMapper directly:

use dev\winterframework\paxb\XmlObjectMapper;
$mapper = new XmlObjectMapper();
$mapper->setParserProperty(XMLReader::VALIDATE);
$product = $mapper->readValue($xml, Product::class, validate: true);
// Serialize back to an XML string
$xmlString = $mapper->writeValue($product);

The same model class can carry both JSON and XML annotations, letting a single DTO deserialize from either format based on the incoming Content-Type.

src/Model/Product.php
use dev\winterframework\stereotype\JsonProperty;
use dev\winterframework\paxb\attr\XmlRootElement;
use dev\winterframework\paxb\attr\XmlElement;
use dev\winterframework\paxb\attr\XmlAttribute;
#[XmlRootElement(name: 'product')]
class Product {
#[JsonProperty(name: 'sku')]
#[XmlAttribute(name: 'sku')]
private string $sku = '';
#[JsonProperty(name: 'name')]
#[XmlElement(name: 'name')]
private string $name = '';
#[JsonProperty(name: 'unit_price')]
#[XmlElement(name: 'unit_price')]
private float $price = 0.0;
#[JsonProperty(name: 'tags', listClass: Tag::class)]
#[XmlElement(name: 'tag', listClass: Tag::class)]
private array $tags = [];
public function getSku(): string { return $this->sku; }
public function getName(): string { return $this->name; }
public function getPrice(): float { return $this->price; }
}

Declare both content types in your controller’s consumes list and the framework handles the rest:

src/Controller/ProductController.php
#[RestController]
class ProductController {
#[RequestMapping(
path: '/api/v1/products',
method: [RequestMethod::POST],
consumes: [MediaType::APPLICATION_JSON, MediaType::APPLICATION_XML]
)]
public function createProduct(
#[RequestBody] Product $product
): ResponseEntity {
return ResponseEntity::created('/api/v1/products/' . $product->getSku())->withJson([
'name' => $product->getName(),
'price' => $product->getPrice(),
]);
}
}