diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index c2331676..3f05dda3 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -163,10 +163,12 @@ stablecoins stdr stretchr superfences +Fatalf Truelayer Trulioo udpa unmarshal +useto viewmodel vulnz Wallex @@ -175,6 +177,7 @@ webassets Worldline Worldpay Xdock +xeipuuv Xendit xerrors xmocksignature @@ -183,3 +186,6 @@ XVCJ Yapily Zalopay Zalora +gojsonschema +quicktype +paymentmethod diff --git a/README.md b/README.md index dfa3d4f2..5412a84d 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,23 @@ generally follow this pattern: 1. Navigate to the Shopping Agent URL and begin engaging. +## JSON Schema Support + +AP2 provides language-agnostic JSON Schema definitions for all mandate types (Intent, Cart, and Payment mandates). These schemas enable: + +- Validation of mandate objects in any programming language +- Type generation for non-Python implementations +- Verifiable Credential creation with proper schema references +- IDE autocomplete and validation support + +**Available schemas:** + +- [`schemas/intent-mandate.schema.json`](schemas/intent-mandate.schema.json) +- [`schemas/cart-mandate.schema.json`](schemas/cart-mandate.schema.json) +- [`schemas/payment-mandate.schema.json`](schemas/payment-mandate.schema.json) + +See the [schemas README](schemas/README.md) for usage examples in Python, JavaScript, Go, and other languages. + ### Installing the AP2 Types Package The protocol's core objects are defined in the [`src/ap2/types`](src/ap2/types) diff --git a/schemas/README.md b/schemas/README.md new file mode 100644 index 00000000..fc35e220 --- /dev/null +++ b/schemas/README.md @@ -0,0 +1,190 @@ +# AP2 JSON Schemas + +This directory contains JSON Schema definitions for the Agent Payments Protocol +(AP2) mandate types. These schemas provide language-agnostic specifications for +validating AP2 mandate objects. + +## Available Schemas + +### Intent Mandate + +- **File**: [`intent-mandate.schema.json`](./intent-mandate.schema.json) +- **Schema ID**: + `https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas/intent-mandate.schema.json` +- **Description**: Represents the user's purchase intent, including + constraints on merchants, SKUs, and refundability. + +### Cart Mandate + +- **File**: [`cart-mandate.schema.json`](./cart-mandate.schema.json) +- **Schema ID**: + `https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas/cart-mandate.schema.json` +- **Description**: A cart whose contents have been digitally signed by the + merchant, serving as a guarantee of items and price for a limited time. + +### Payment Mandate + +- **File**: [`payment-mandate.schema.json`](./payment-mandate.schema.json) +- **Schema ID**: + `https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas/payment-mandate.schema.json` +- **Description**: Contains the user's instructions and authorization for + payment, shared with the payments ecosystem. + +## Usage + +### Validating JSON Objects + +These schemas can be used with any JSON Schema validator to ensure mandate +objects conform to the AP2 specification. + +#### Python Example + +```python +import json +import jsonschema +import requests + +# Load the schema +schema_url = "https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas/intent-mandate.schema.json" +schema = requests.get(schema_url).json() + +# Your mandate object +mandate = { + "natural_language_description": "High top, old school, red basketball shoes", + "intent_expiry": "2025-12-31T23:59:59Z", + "user_cart_confirmation_required": True +} + +# Validate +jsonschema.validate(instance=mandate, schema=schema) +print("✓ Mandate is valid!") +``` + +#### JavaScript/TypeScript Example + +```typescript +import Ajv from "ajv"; + +const ajv = new Ajv(); + +// Load schema +const schema = await fetch( + "https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas/intent-mandate.schema.json" +).then((r) => r.json()); + +const validate = ajv.compile(schema); + +const mandate = { + natural_language_description: "High top, old school, red basketball shoes", + intent_expiry: "2025-12-31T23:59:59Z", + user_cart_confirmation_required: true, +}; + +if (validate(mandate)) { + console.log("✓ Mandate is valid!"); +} else { + console.error("Validation errors:", validate.errors); +} +``` + +#### Go Example + +```go +import ( + "encoding/json" + "fmt" + "log" + "github.com/xeipuuv/gojsonschema" +) + +// Load schema +schemaLoader := gojsonschema.NewReferenceLoader( + "https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas/intent-mandate.schema.json", +) + +// Your mandate +mandate := map[string]interface{}{ + "natural_language_description": "High top, old school, red basketball shoes", + "intent_expiry": "2025-12-31T23:59:59Z", + "user_cart_confirmation_required": true, +} +mandateLoader := gojsonschema.NewGoLoader(mandate) + +// Validate +result, err := gojsonschema.Validate(schemaLoader, mandateLoader) +if err != nil { + log.Fatalf("Validation error: %v", err) +} + +if result.Valid() { + fmt.Println("✓ Mandate is valid!") +} else { + fmt.Println("Validation errors:") + for _, desc := range result.Errors() { + fmt.Printf("- %s\n", desc) + } +} +``` + +### Generating Type Definitions + +You can use tools like `json-schema-to-typescript` or `quicktype` to generate +type definitions from these schemas: + +```bash +# TypeScript +npx json-schema-to-typescript schemas/intent-mandate.schema.json > IntentMandate.ts + +# Multiple languages with quicktype +quicktype schemas/intent-mandate.schema.json -o IntentMandate.swift +quicktype schemas/intent-mandate.schema.json -o IntentMandate.kt +``` + +### Using with Verifiable Credentials + +These schemas are designed to work with Verifiable Credentials (VCs). When +creating a VC that contains an AP2 mandate, reference the appropriate schema: + +```json +{ + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiableCredential", "AP2IntentMandate"], + "credentialSubject": { + "$schema": "https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas/intent-mandate.schema.json", + "natural_language_description": "High top, old school, red basketball shoes", + "intent_expiry": "2025-12-31T23:59:59Z", + "user_cart_confirmation_required": true + } +} +``` + +## Regenerating Schemas + +If the Python type definitions in +[`src/ap2/types/mandate.py`](../src/ap2/types/mandate.py) are updated, you can +regenerate these schemas by running: + +```bash +python3 scripts/generate_schemas.py +``` + +This script uses Pydantic's built-in JSON Schema generation to ensure the +schemas stay in sync with the Python implementation. + +## Schema Versioning + +These schemas follow the AP2 protocol versioning. When breaking changes are made +to the protocol, new schema versions will be created with appropriate version +tags. + +## Related Documentation + +- [AP2 Specification](../docs/specification.md) - Full protocol specification +- [A2A Extension](../docs/a2a-extension.md) - Details on using AP2 with A2A +- [Python Type Definitions](../src/ap2/types/) - Source Pydantic models + +## Contributing + +If you find issues with these schemas or have suggestions for improvements, +please [open an issue](https://github.com/google-agentic-commerce/AP2/issues) or +submit a pull request. diff --git a/schemas/cart-mandate.schema.json b/schemas/cart-mandate.schema.json new file mode 100644 index 00000000..8cfb910b --- /dev/null +++ b/schemas/cart-mandate.schema.json @@ -0,0 +1,564 @@ +{ + "$defs": { + "CartContents": { + "description": "The detailed contents of a cart.\n\nThis object is signed by the merchant to create a CartMandate.", + "properties": { + "id": { + "description": "A unique identifier for this cart.", + "title": "Id", + "type": "string" + }, + "user_cart_confirmation_required": { + "description": "If true, the merchant requires the user to confirm the cart before the purchase can be completed.", + "title": "User Cart Confirmation Required", + "type": "boolean" + }, + "payment_request": { + "$ref": "#/$defs/PaymentRequest", + "description": "The W3C PaymentRequest object to initiate payment. This contains the items being purchased, prices, and the set of payment methods accepted by the merchant for this cart." + }, + "cart_expiry": { + "description": "When this cart expires, in ISO 8601 format.", + "title": "Cart Expiry", + "type": "string", + "format": "date-time" + }, + "merchant_name": { + "description": "The name of the merchant.", + "title": "Merchant Name", + "type": "string" + } + }, + "required": [ + "id", + "user_cart_confirmation_required", + "payment_request", + "cart_expiry", + "merchant_name" + ], + "title": "CartContents", + "type": "object" + }, + "ContactAddress": { + "description": "The ContactAddress interface represents a physical address.\n\nSpecification:\nhttps://www.w3.org/TR/contact-picker/#contact-address", + "properties": { + "city": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "City" + }, + "country": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Country" + }, + "dependent_locality": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dependent Locality" + }, + "organization": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Organization" + }, + "phone_number": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Phone Number" + }, + "postal_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Postal Code" + }, + "recipient": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Recipient" + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Region" + }, + "sorting_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sorting Code" + }, + "address_line": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Address Line" + } + }, + "title": "ContactAddress", + "type": "object" + }, + "PaymentCurrencyAmount": { + "description": "A PaymentCurrencyAmount is used to supply monetary amounts.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#dom-paymentcurrencyamount", + "properties": { + "currency": { + "description": "The three-letter ISO 4217 currency code.", + "title": "Currency", + "type": "string" + }, + "value": { + "description": "The monetary value.", + "title": "Value", + "type": "number" + } + }, + "required": [ + "currency", + "value" + ], + "title": "PaymentCurrencyAmount", + "type": "object" + }, + "PaymentDetailsInit": { + "description": "Contains the details of the payment being requested.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#dom-paymentdetailsinit", + "properties": { + "id": { + "description": "A unique identifier for the payment request.", + "title": "Id", + "type": "string" + }, + "display_items": { + "description": "A list of payment items to be displayed to the user.", + "items": { + "$ref": "#/$defs/PaymentItem" + }, + "title": "Display Items", + "type": "array" + }, + "shipping_options": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/PaymentShippingOption" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A list of available shipping options.", + "title": "Shipping Options" + }, + "modifiers": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/PaymentDetailsModifier" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A list of price modifiers for particular payment methods.", + "title": "Modifiers" + }, + "total": { + "$ref": "#/$defs/PaymentItem", + "description": "The total payment amount." + } + }, + "required": [ + "id", + "display_items", + "total" + ], + "title": "PaymentDetailsInit", + "type": "object" + }, + "PaymentDetailsModifier": { + "description": "Provides details that modify the payment details based on a payment method.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#dom-paymentdetailsmodifier", + "properties": { + "supported_methods": { + "description": "The payment method ID that this modifier applies to.", + "title": "Supported Methods", + "type": "string" + }, + "total": { + "anyOf": [ + { + "$ref": "#/$defs/PaymentItem" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A PaymentItem value that overrides the original item total." + }, + "additional_display_items": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/PaymentItem" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Additional PaymentItems applicable for this payment method.", + "title": "Additional Display Items" + }, + "data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Payment method specific data for the modifier.", + "title": "Data" + } + }, + "required": [ + "supported_methods" + ], + "title": "PaymentDetailsModifier", + "type": "object" + }, + "PaymentItem": { + "description": "An item for purchase and the value asked for it.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#dom-paymentitem", + "properties": { + "label": { + "description": "A human-readable description of the item.", + "title": "Label", + "type": "string" + }, + "amount": { + "$ref": "#/$defs/PaymentCurrencyAmount", + "description": "The monetary amount of the item." + }, + "pending": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If true, indicates the amount is not final.", + "title": "Pending" + }, + "refund_period": { + "default": 30, + "description": "The refund duration for this item, in days.", + "title": "Refund Period", + "type": "integer" + } + }, + "required": [ + "label", + "amount" + ], + "title": "PaymentItem", + "type": "object" + }, + "PaymentMethodData": { + "description": "Indicates a payment method and associated data specific to the method.\n\nFor example:\n- A card may have a processing fee if it is used.\n- A loyalty card may offer a discount on the purchase.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#dom-paymentmethoddata", + "properties": { + "supported_methods": { + "description": "A string identifying the payment method.", + "title": "Supported Methods", + "type": "string" + }, + "data": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Payment method specific details.", + "title": "Data" + } + }, + "required": [ + "supported_methods" + ], + "title": "PaymentMethodData", + "type": "object" + }, + "PaymentOptions": { + "description": "Information about the eligible payment options for the payment request.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#dom-paymentoptions", + "properties": { + "request_payer_name": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Indicates if the payer's name should be collected.", + "title": "Request Payer Name" + }, + "request_payer_email": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Indicates if the payer's email should be collected.", + "title": "Request Payer Email" + }, + "request_payer_phone": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Indicates if the payer's phone number should be collected.", + "title": "Request Payer Phone" + }, + "request_shipping": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": true, + "description": "Indicates if the payer's shipping address should be collected.", + "title": "Request Shipping" + }, + "shipping_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Can be `shipping`, `delivery`, or `pickup`.", + "title": "Shipping Type" + } + }, + "title": "PaymentOptions", + "type": "object" + }, + "PaymentRequest": { + "description": "A request for payment.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#paymentrequest-interface", + "properties": { + "method_data": { + "description": "A list of supported payment methods.", + "items": { + "$ref": "#/$defs/PaymentMethodData" + }, + "title": "Method Data", + "type": "array" + }, + "details": { + "$ref": "#/$defs/PaymentDetailsInit", + "description": "The financial details of the transaction." + }, + "options": { + "anyOf": [ + { + "$ref": "#/$defs/PaymentOptions" + }, + { + "type": "null" + } + ], + "default": null + }, + "shipping_address": { + "anyOf": [ + { + "$ref": "#/$defs/ContactAddress" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The user's provided shipping address." + } + }, + "required": [ + "method_data", + "details" + ], + "title": "PaymentRequest", + "type": "object" + }, + "PaymentShippingOption": { + "description": "Describes a shipping option.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#dom-paymentshippingoption", + "properties": { + "id": { + "description": "A unique identifier for the shipping option.", + "title": "Id", + "type": "string" + }, + "label": { + "description": "A human-readable description of the shipping option.", + "title": "Label", + "type": "string" + }, + "amount": { + "$ref": "#/$defs/PaymentCurrencyAmount", + "description": "The cost of this shipping option." + }, + "selected": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "If true, indicates this as the default option.", + "title": "Selected" + } + }, + "required": [ + "id", + "label", + "amount" + ], + "title": "PaymentShippingOption", + "type": "object" + } + }, + "description": "A cart whose contents have been digitally signed by the merchant.\n\nThis serves as a guarantee of the items and price for a limited time.", + "properties": { + "contents": { + "$ref": "#/$defs/CartContents", + "description": "The contents of the cart." + }, + "merchant_authorization": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A base64url-encoded JSON Web Token (JWT) that digitally\n signs the cart contents, guaranteeing its authenticity and integrity:\n 1. Header includes the signing algorithm and key ID.\n 2. Payload includes:\n - iss, sub, aud: Identifiers for the merchant (issuer)\n and the intended recipient (audience), like a payment processor.\n - iat: iat, exp: Timestamps for the token's creation and its\n short-lived expiration (e.g., 5-15 minutes) to enhance security.\n - jti: Unique identifier for the JWT to prevent replay attacks.\n - cart_hash: A secure hash of the CartMandate, ensuring\n integrity. The hash is computed over the canonical JSON\n representation of the CartContents object.\n 3. Signature: A digital signature created with the merchant's private\n key. It allows anyone with the public key to verify the token's\n authenticity and confirm that the payload has not been tampered with.\n The entire JWT is base64url encoded to ensure safe transmission.", + "example": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjQwOTA...", + "title": "Merchant Authorization" + } + }, + "required": [ + "contents" + ], + "title": "CartMandate", + "type": "object", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas/cart-mandate.schema.json" +} diff --git a/schemas/intent-mandate.schema.json b/schemas/intent-mandate.schema.json new file mode 100644 index 00000000..03c315d5 --- /dev/null +++ b/schemas/intent-mandate.schema.json @@ -0,0 +1,76 @@ +{ + "description": "Represents the user's purchase intent.\n\nThese are the initial fields utilized in the human-present flow. For\nhuman-not-present flows, additional fields will be added to this mandate.", + "properties": { + "user_cart_confirmation_required": { + "default": true, + "description": "If false, the agent can make purchases on the user's behalf once all purchase conditions have been satisfied. This must be true if the intent mandate is not signed by the user.", + "title": "User Cart Confirmation Required", + "type": "boolean" + }, + "natural_language_description": { + "description": "The natural language description of the user's intent. This is generated by the shopping agent, and confirmed by the user. The goal is to have informed consent by the user.", + "example": "High top, old school, red basketball shoes", + "title": "Natural Language Description", + "type": "string" + }, + "merchants": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Merchants allowed to fulfill the intent. If not set, the shopping agent is able to work with any suitable merchant.", + "title": "Merchants" + }, + "skus": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A list of specific product SKUs. If not set, any SKU is allowed.", + "title": "Skus" + }, + "requires_refundability": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "If true, items must be refundable.", + "title": "Requires Refundability" + }, + "intent_expiry": { + "description": "When the intent mandate expires, in ISO 8601 format.", + "title": "Intent Expiry", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "natural_language_description", + "intent_expiry" + ], + "title": "IntentMandate", + "type": "object", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas/intent-mandate.schema.json" +} diff --git a/schemas/payment-mandate.schema.json b/schemas/payment-mandate.schema.json new file mode 100644 index 00000000..26f2a4ea --- /dev/null +++ b/schemas/payment-mandate.schema.json @@ -0,0 +1,397 @@ +{ + "$defs": { + "ContactAddress": { + "description": "The ContactAddress interface represents a physical address.\n\nSpecification:\nhttps://www.w3.org/TR/contact-picker/#contact-address", + "properties": { + "city": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "City" + }, + "country": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Country" + }, + "dependent_locality": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dependent Locality" + }, + "organization": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Organization" + }, + "phone_number": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Phone Number" + }, + "postal_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Postal Code" + }, + "recipient": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Recipient" + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Region" + }, + "sorting_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sorting Code" + }, + "address_line": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Address Line" + } + }, + "title": "ContactAddress", + "type": "object" + }, + "PaymentCurrencyAmount": { + "description": "A PaymentCurrencyAmount is used to supply monetary amounts.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#dom-paymentcurrencyamount", + "properties": { + "currency": { + "description": "The three-letter ISO 4217 currency code.", + "title": "Currency", + "type": "string" + }, + "value": { + "description": "The monetary value.", + "title": "Value", + "type": "number" + } + }, + "required": [ + "currency", + "value" + ], + "title": "PaymentCurrencyAmount", + "type": "object" + }, + "PaymentItem": { + "description": "An item for purchase and the value asked for it.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#dom-paymentitem", + "properties": { + "label": { + "description": "A human-readable description of the item.", + "title": "Label", + "type": "string" + }, + "amount": { + "$ref": "#/$defs/PaymentCurrencyAmount", + "description": "The monetary amount of the item." + }, + "pending": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "If true, indicates the amount is not final.", + "title": "Pending" + }, + "refund_period": { + "default": 30, + "description": "The refund duration for this item, in days.", + "title": "Refund Period", + "type": "integer" + } + }, + "required": [ + "label", + "amount" + ], + "title": "PaymentItem", + "type": "object" + }, + "PaymentMandateContents": { + "description": "The data contents of a PaymentMandate.", + "properties": { + "payment_mandate_id": { + "description": "A unique identifier for this payment mandate.", + "title": "Payment Mandate Id", + "type": "string" + }, + "payment_details_id": { + "description": "A unique identifier for the payment request.", + "title": "Payment Details Id", + "type": "string" + }, + "payment_details_total": { + "$ref": "#/$defs/PaymentItem", + "description": "The total payment amount." + }, + "payment_response": { + "$ref": "#/$defs/PaymentResponse", + "description": "The payment response containing details of the payment method chosen by the user." + }, + "merchant_agent": { + "description": "Identifier for the merchant.", + "title": "Merchant Agent", + "type": "string" + }, + "timestamp": { + "description": "The date and time the mandate was created, in ISO 8601 format.", + "title": "Timestamp", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "payment_mandate_id", + "payment_details_id", + "payment_details_total", + "payment_response", + "merchant_agent" + ], + "title": "PaymentMandateContents", + "type": "object" + }, + "PaymentResponse": { + "description": "Indicates a user has chosen a payment method & approved a payment request.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#paymentresponse-interface", + "properties": { + "request_id": { + "description": "The unique ID from the original PaymentRequest.", + "title": "Request Id", + "type": "string" + }, + "method_name": { + "description": "The payment method chosen by the user.", + "title": "Method Name", + "type": "string" + }, + "details": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A dictionary generated by a payment method that a merchant can use to process a transaction. The contents will depend upon the payment method.", + "title": "Details" + }, + "shipping_address": { + "anyOf": [ + { + "$ref": "#/$defs/ContactAddress" + }, + { + "type": "null" + } + ], + "default": null + }, + "shipping_option": { + "anyOf": [ + { + "$ref": "#/$defs/PaymentShippingOption" + }, + { + "type": "null" + } + ], + "default": null + }, + "payer_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Payer Name" + }, + "payer_email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Payer Email" + }, + "payer_phone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Payer Phone" + } + }, + "required": [ + "request_id", + "method_name" + ], + "title": "PaymentResponse", + "type": "object" + }, + "PaymentShippingOption": { + "description": "Describes a shipping option.\n\nSpecification:\nhttps://www.w3.org/TR/payment-request/#dom-paymentshippingoption", + "properties": { + "id": { + "description": "A unique identifier for the shipping option.", + "title": "Id", + "type": "string" + }, + "label": { + "description": "A human-readable description of the shipping option.", + "title": "Label", + "type": "string" + }, + "amount": { + "$ref": "#/$defs/PaymentCurrencyAmount", + "description": "The cost of this shipping option." + }, + "selected": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "If true, indicates this as the default option.", + "title": "Selected" + } + }, + "required": [ + "id", + "label", + "amount" + ], + "title": "PaymentShippingOption", + "type": "object" + } + }, + "description": "Contains the user's instructions & authorization for payment.\n\nWhile the Cart and Intent mandates are required by the merchant to fulfill the\norder, separately the protocol provides additional visibility into the agentic\ntransaction to the payments ecosystem. For this purpose, the PaymentMandate\n(bound to Cart/Intent mandate but containing separate information) may be\nshared with the network/issuer along with the standard transaction\nauthorization messages. The goal of the PaymentMandate is to help the\nnetwork/issuer build trust into the agentic transaction.", + "properties": { + "payment_mandate_contents": { + "$ref": "#/$defs/PaymentMandateContents", + "description": "The data contents of the payment mandate." + }, + "user_authorization": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "This is a base64_url-encoded verifiable presentation of a verifiable\ncredential signing over the cart_mandate and payment_mandate_hashes.\nFor example an sd-jwt-vc would contain:\n\n- An issuer-signed jwt authorizing a 'cnf' claim\n- A key-binding jwt with the claims\n \"aud\": ...\n \"nonce\": ...\n \"sd_hash\": hash of the issuer-signed jwt\n \"transaction_data\": an array containing the secure hashes of \n CartMandate and PaymentMandateContents.", + "example": "eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6ZXhhbXBsZ...", + "title": "User Authorization" + } + }, + "required": [ + "payment_mandate_contents" + ], + "title": "PaymentMandate", + "type": "object", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas/payment-mandate.schema.json" +} diff --git a/scripts/generate_schemas.py b/scripts/generate_schemas.py new file mode 100755 index 00000000..c7f98eda --- /dev/null +++ b/scripts/generate_schemas.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generate JSON Schema definitions for AP2 mandate types. + +This script generates JSON Schema files from the Pydantic models defined in +the AP2 types package. These schemas can be used for validation in any +programming language and are essential for Verifiable Credential implementations. + +Usage: + python scripts/generate_schemas.py + +Requirements: + pip install pydantic --break-system-packages --user + or use: uv pip install pydantic +""" + +import json +import sys +import textwrap +from pathlib import Path + +# Add the src directory to the Python path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +try: + # Import the mandate types + from ap2.types.mandate import CartMandate + from ap2.types.mandate import IntentMandate + from ap2.types.mandate import PaymentMandate +except ImportError as e: + print(f"Error importing AP2 types: {e}") + print("\nPlease install pydantic:") + print(" pip install pydantic --break-system-packages --user") + print(" or: uv pip install pydantic") + sys.exit(1) + + +def generate_schema(model_class, output_path: Path, schema_id: str) -> None: + """Generate JSON Schema for a Pydantic model and write to file. + + Args: + model_class: The Pydantic model class to generate schema from. + output_path: Path where the schema file should be written. + schema_id: The $id for the JSON Schema. + """ + # Generate the schema using Pydantic's built-in method + schema = model_class.model_json_schema(mode="serialization") + + # Post-process the schema to improve formatting and add details + def _post_process(node): + if isinstance(node, dict): + if "description" in node and isinstance(node["description"], str): + node["description"] = textwrap.dedent(node["description"]).strip() + + if "properties" in node and isinstance(node["properties"], dict): + for prop_name, prop_schema in node["properties"].items(): + if prop_name in ("intent_expiry", "cart_expiry", "timestamp"): + if prop_schema.get("type") == "string": + prop_schema["format"] = "date-time" + + for value in node.values(): + _post_process(value) + elif isinstance(node, list): + for item in node: + _post_process(item) + + _post_process(schema) + + # Add $schema and $id fields for proper JSON Schema compliance + schema["$schema"] = "https://json-schema.org/draft/2020-12/schema" + schema["$id"] = schema_id + + # Add metadata + schema["title"] = model_class.__name__ + if model_class.__doc__: + schema["description"] = textwrap.dedent(model_class.__doc__).strip() + + # Write schema to file with pretty formatting + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + json.dump(schema, f, indent=2, ensure_ascii=False) + f.write("\n") # Add trailing newline + + print(f"✓ Generated {output_path}") + + +def main(): + """Generate all mandate JSON schemas.""" + # Define the output directory + schemas_dir = Path(__file__).parent.parent / "schemas" + + # Define the base URL for schema IDs (can be updated when hosted publicly) + base_url = "https://raw.githubusercontent.com/google-agentic-commerce/AP2/main/schemas" + + # Generate schemas for each mandate type + schemas_to_generate = [ + { + "model": IntentMandate, + "filename": "intent-mandate.schema.json", + "id": f"{base_url}/intent-mandate.schema.json", + }, + { + "model": CartMandate, + "filename": "cart-mandate.schema.json", + "id": f"{base_url}/cart-mandate.schema.json", + }, + { + "model": PaymentMandate, + "filename": "payment-mandate.schema.json", + "id": f"{base_url}/payment-mandate.schema.json", + }, + ] + + print("Generating JSON Schemas for AP2 mandate types...\n") + + for schema_config in schemas_to_generate: + output_path = schemas_dir / schema_config["filename"] + generate_schema( + schema_config["model"], + output_path, + schema_config["id"], + ) + + print(f"\n✓ All schemas generated successfully in {schemas_dir}/") + print("\nThese schemas can be used for:") + print(" • Validating mandate objects in any programming language") + print(" • Generating type definitions for non-Python implementations") + print(" • Creating Verifiable Credentials with proper schema references") + print(" • IDE autocomplete and validation support") + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_schemas.py b/scripts/validate_schemas.py new file mode 100755 index 00000000..9e5ad46d --- /dev/null +++ b/scripts/validate_schemas.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test script to validate the generated JSON schemas.""" + +import json +import sys +from pathlib import Path + +try: + import jsonschema +except ImportError as e: + print(f"Error importing jsonschema: {e}") + print("\nPlease install jsonschema:") + print(" pip install jsonschema --break-system-packages --user") + print(" or: uv pip install jsonschema") + sys.exit(1) + +# Add src to path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from ap2.types.mandate import IntentMandate, CartMandate, PaymentMandate + + +def test_schema(schema_path: Path, example_instance: dict, name: str): + """Test that a schema validates an example instance.""" + print(f"\nTesting {name}...") + + # Load schema + with open(schema_path) as f: + schema = json.load(f) + + # Validate the instance + try: + jsonschema.validate(instance=example_instance, schema=schema) + print(f" ✓ Schema is valid") + print(f" ✓ Example instance validates successfully") + return True + except jsonschema.exceptions.ValidationError as e: + print(f" ✗ Validation error: {e.message}") + return False + except jsonschema.exceptions.SchemaError as e: + print(f" ✗ Schema error: {e.message}") + return False + + +def main(): + schemas_dir = Path(__file__).parent.parent / "schemas" + + print("=" * 60) + print("AP2 JSON Schema Validation Test") + print("=" * 60) + + # Test IntentMandate + intent_example = { + "natural_language_description": "High top, old school, red basketball shoes", + "intent_expiry": "2025-12-31T23:59:59Z", + "user_cart_confirmation_required": True, + "merchants": ["example-merchant.com"], + "requires_refundability": True + } + + intent_result = test_schema( + schemas_dir / "intent-mandate.schema.json", + intent_example, + "IntentMandate" + ) + + # Test CartMandate + cart_example = { + "contents": { + "id": "cart-123", + "user_cart_confirmation_required": True, + "payment_request": { + "method_data": [ + { + "supported_methods": "https://example.com/pay", + "data": {} + } + ], + "details": { + "id": "payment-req-123", + "total": { + "label": "Total", + "amount": { + "currency": "USD", + "value": 99.99 + } + }, + "display_items": [] + } + }, + "cart_expiry": "2025-12-20T23:59:59Z", + "merchant_name": "Example Merchant" + }, + "merchant_authorization": None + } + + cart_result = test_schema( + schemas_dir / "cart-mandate.schema.json", + cart_example, + "CartMandate" + ) + + # Test PaymentMandate + payment_example = { + "payment_mandate_contents": { + "payment_mandate_id": "pm-123", + "payment_details_id": "pd-123", + "payment_details_total": { + "label": "Total", + "amount": { + "currency": "USD", + "value": 99.99 + } + }, + "payment_response": { + "request_id": "payment-req-123", + "method_name": "https://example.com/pay", + "details": {} + }, + "merchant_agent": "merchant-agent-123", + "timestamp": "2025-12-18T10:00:00Z" + }, + "user_authorization": None + } + + payment_result = test_schema( + schemas_dir / "payment-mandate.schema.json", + payment_example, + "PaymentMandate" + ) + + print("\n" + "=" * 60) + print("Summary:") + print("=" * 60) + print(f" IntentMandate: {'✓ PASS' if intent_result else '✗ FAIL'}") + print(f" CartMandate: {'✓ PASS' if cart_result else '✗ FAIL'}") + print(f" PaymentMandate: {'✓ PASS' if payment_result else '✗ FAIL'}") + print("=" * 60) + + if all([intent_result, cart_result, payment_result]): + print("\n✓ All schemas validated successfully!") + return 0 + else: + print("\n✗ Some schemas failed validation") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/ap2/types/payment_request.py b/src/ap2/types/payment_request.py index 22495028..72439045 100644 --- a/src/ap2/types/payment_request.py +++ b/src/ap2/types/payment_request.py @@ -217,8 +217,8 @@ class PaymentResponse(BaseModel): details: Optional[Dict[str, Any]] = Field( None, description=( - "A dictionary generated by a payment method that a merchant can use" - "to process a transaction. The contents will depend upon the payment" + "A dictionary generated by a payment method that a merchant can use " + "to process a transaction. The contents will depend upon the payment " "method." ), )